mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
1197 lines
46 KiB
C++
1197 lines
46 KiB
C++
/*
|
|
* GDevelop JS Platform
|
|
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
|
* reserved. This project is released under the MIT License.
|
|
*/
|
|
#include "GDJS/IDE/ExporterHelper.h"
|
|
|
|
#if defined(EMSCRIPTEN)
|
|
#include <emscripten.h>
|
|
#endif
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <sstream>
|
|
#include <streambuf>
|
|
#include <string>
|
|
|
|
#include "GDCore/CommonTools.h"
|
|
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
|
|
#include "GDCore/Events/CodeGeneration/EffectsCodeGenerator.h"
|
|
#include "GDCore/Extensions/Metadata/DependencyMetadata.h"
|
|
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
|
|
#include "GDCore/Extensions/Platform.h"
|
|
#include "GDCore/Extensions/PlatformExtension.h"
|
|
#include "GDCore/IDE/AbstractFileSystem.h"
|
|
#include "GDCore/IDE/CaptureOptions.h"
|
|
#include "GDCore/IDE/Events/UsedExtensionsFinder.h"
|
|
#include "GDCore/IDE/ExportedDependencyResolver.h"
|
|
#include "GDCore/IDE/Project/ProjectResourcesCopier.h"
|
|
#include "GDCore/IDE/Project/SceneResourcesFinder.h"
|
|
#include "GDCore/IDE/ProjectStripper.h"
|
|
#include "GDCore/IDE/SceneNameMangler.h"
|
|
#include "GDCore/Project/EventsBasedObject.h"
|
|
#include "GDCore/Project/EventsFunctionsExtension.h"
|
|
#include "GDCore/Project/ExternalEvents.h"
|
|
#include "GDCore/Project/ExternalLayout.h"
|
|
#include "GDCore/Project/Layout.h"
|
|
#include "GDCore/Project/Project.h"
|
|
#include "GDCore/Project/PropertyDescriptor.h"
|
|
#include "GDCore/Project/SourceFile.h"
|
|
#include "GDCore/Serialization/Serializer.h"
|
|
#include "GDCore/Tools/Localization.h"
|
|
#include "GDCore/Tools/Log.h"
|
|
#include "GDJS/Events/CodeGeneration/LayoutCodeGenerator.h"
|
|
#include "GDJS/Extensions/JsPlatform.h"
|
|
#undef CopyFile // Disable an annoying macro
|
|
|
|
namespace {
|
|
double GetTimeNow() {
|
|
#if defined(EMSCRIPTEN)
|
|
double currentTime = emscripten_get_now();
|
|
return currentTime;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
double GetTimeSpent(double previousTime) { return GetTimeNow() - previousTime; }
|
|
double LogTimeSpent(const gd::String &name, double previousTime) {
|
|
gd::LogStatus(name + " took " + gd::String::From(GetTimeSpent(previousTime)) +
|
|
"ms");
|
|
std::cout << std::endl;
|
|
return GetTimeNow();
|
|
}
|
|
} // namespace
|
|
|
|
namespace gdjs {
|
|
|
|
static void InsertUnique(std::vector<gd::String> &container, gd::String str) {
|
|
if (std::find(container.begin(), container.end(), str) == container.end())
|
|
container.push_back(str);
|
|
}
|
|
|
|
static gd::String CleanProjectName(gd::String projectName) {
|
|
gd::String partiallyCleanedProjectName = projectName;
|
|
|
|
static const gd::String forbiddenFileNameCharacters =
|
|
"\\/:*?\"<>|"; // See
|
|
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
|
|
|
for (size_t i = 0; i < partiallyCleanedProjectName.size();) {
|
|
// Delete all characters that are not allowed in a filename
|
|
if (forbiddenFileNameCharacters.find(partiallyCleanedProjectName[i]) !=
|
|
gd::String::npos) {
|
|
partiallyCleanedProjectName.erase(i, 1);
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (partiallyCleanedProjectName.empty())
|
|
partiallyCleanedProjectName = "Project";
|
|
|
|
return partiallyCleanedProjectName;
|
|
}
|
|
|
|
ExporterHelper::ExporterHelper(gd::AbstractFileSystem &fileSystem,
|
|
gd::String gdjsRoot_,
|
|
gd::String codeOutputDir_)
|
|
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_) {};
|
|
|
|
bool ExporterHelper::ExportProjectForPixiPreview(
|
|
const PreviewExportOptions &options) {
|
|
double previousTime = GetTimeNow();
|
|
fs.MkDir(options.exportPath);
|
|
fs.ClearDir(options.exportPath);
|
|
std::vector<gd::String> includesFiles;
|
|
std::vector<gd::String> resourcesFiles;
|
|
|
|
// TODO Try to remove side effects to avoid the copy
|
|
// that destroys the AST in cache.
|
|
gd::Project exportedProject = options.project;
|
|
const gd::Project &immutableProject = exportedProject;
|
|
|
|
if (options.fullLoadingScreen) {
|
|
// Use project properties fallback to set empty properties
|
|
if (exportedProject.GetAuthorIds().empty() &&
|
|
!options.fallbackAuthorId.empty()) {
|
|
exportedProject.GetAuthorIds().push_back(options.fallbackAuthorId);
|
|
}
|
|
if (exportedProject.GetAuthorUsernames().empty() &&
|
|
!options.fallbackAuthorUsername.empty()) {
|
|
exportedProject.GetAuthorUsernames().push_back(
|
|
options.fallbackAuthorUsername);
|
|
}
|
|
} else {
|
|
// Most of the time, we skip the logo and minimum duration so that
|
|
// the preview start as soon as possible.
|
|
exportedProject.GetLoadingScreen()
|
|
.ShowGDevelopLogoDuringLoadingScreen(false)
|
|
.SetMinDuration(0);
|
|
exportedProject.GetWatermark().ShowGDevelopWatermark(false);
|
|
}
|
|
|
|
// Export resources (*before* generating events as some resources filenames
|
|
// may be updated)
|
|
ExportResources(fs, exportedProject, options.exportPath);
|
|
|
|
previousTime = LogTimeSpent("Resource export", previousTime);
|
|
|
|
// Compatibility with GD <= 5.0-beta56
|
|
// Stay compatible with text objects declaring their font as just a filename
|
|
// without a font resource - by manually adding these resources.
|
|
AddDeprecatedFontFilesToFontResources(
|
|
fs, exportedProject.GetResourcesManager(), options.exportPath);
|
|
// end of compatibility code
|
|
|
|
auto usedExtensionsResult =
|
|
gd::UsedExtensionsFinder::ScanProject(exportedProject);
|
|
|
|
// Export engine libraries
|
|
AddLibsInclude(/*pixiRenderers=*/true,
|
|
usedExtensionsResult.Has3DObjects(),
|
|
/*includeWebsocketDebuggerClient=*/
|
|
!options.websocketDebuggerServerAddress.empty(),
|
|
/*includeWindowMessageDebuggerClient=*/
|
|
options.useWindowMessageDebuggerClient,
|
|
/*includeMinimalDebuggerClient=*/
|
|
options.useMinimalDebuggerClient,
|
|
/*includeCaptureManager=*/
|
|
!options.captureOptions.IsEmpty(),
|
|
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
|
|
includesFiles);
|
|
|
|
// Export files for free function, object and behaviors
|
|
for (const auto &includeFile : usedExtensionsResult.GetUsedIncludeFiles()) {
|
|
InsertUnique(includesFiles, includeFile);
|
|
}
|
|
for (const auto &requiredFile : usedExtensionsResult.GetUsedRequiredFiles()) {
|
|
InsertUnique(resourcesFiles, requiredFile);
|
|
}
|
|
|
|
// Export effects (after engine libraries as they auto-register themselves to
|
|
// the engine)
|
|
ExportEffectIncludes(exportedProject, includesFiles);
|
|
|
|
previousTime = LogTimeSpent("Include files export", previousTime);
|
|
|
|
if (!options.projectDataOnlyExport) {
|
|
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport =
|
|
options.project.GetWholeProjectDiagnosticReport();
|
|
wholeProjectDiagnosticReport.Clear();
|
|
|
|
// Generate events code
|
|
if (!ExportEventsCode(immutableProject,
|
|
codeOutputDir,
|
|
includesFiles,
|
|
wholeProjectDiagnosticReport,
|
|
true)) {
|
|
return false;
|
|
}
|
|
|
|
// Export source files
|
|
if (!ExportExternalSourceFiles(
|
|
immutableProject, codeOutputDir, includesFiles)) {
|
|
gd::LogError(
|
|
_("Error during exporting! Unable to export source files:\n") +
|
|
lastError);
|
|
return false;
|
|
}
|
|
|
|
previousTime = LogTimeSpent("Events code export", previousTime);
|
|
}
|
|
|
|
auto projectUsedResources =
|
|
gd::SceneResourcesFinder::FindProjectResources(exportedProject);
|
|
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
|
|
for (std::size_t layoutIndex = 0;
|
|
layoutIndex < exportedProject.GetLayoutsCount();
|
|
layoutIndex++) {
|
|
auto &layout = exportedProject.GetLayout(layoutIndex);
|
|
scenesUsedResources[layout.GetName()] =
|
|
gd::SceneResourcesFinder::FindSceneResources(exportedProject, layout);
|
|
}
|
|
|
|
// Strip the project (*after* generating events as the events may use stripped
|
|
// things (objects groups...))
|
|
gd::ProjectStripper::StripProjectForExport(exportedProject);
|
|
|
|
previousTime = LogTimeSpent("Data stripping", previousTime);
|
|
|
|
// Create the setup options passed to the gdjs.RuntimeGame
|
|
gd::SerializerElement runtimeGameOptions;
|
|
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
|
|
|
|
auto &initialRuntimeGameStatus =
|
|
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
|
|
initialRuntimeGameStatus.AddChild("sceneName")
|
|
.SetStringValue(options.layoutName);
|
|
if (options.isInGameEdition) {
|
|
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
|
|
}
|
|
if (!options.externalLayoutName.empty()) {
|
|
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
|
|
.SetValue(options.externalLayoutName);
|
|
|
|
if (options.isInGameEdition) {
|
|
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
|
|
.SetBoolValue(true);
|
|
}
|
|
}
|
|
|
|
runtimeGameOptions.AddChild("projectDataOnlyExport")
|
|
.SetBoolValue(options.projectDataOnlyExport);
|
|
runtimeGameOptions.AddChild("nativeMobileApp")
|
|
.SetBoolValue(options.nativeMobileApp);
|
|
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
|
|
.SetStringValue(options.websocketDebuggerServerAddress);
|
|
runtimeGameOptions.AddChild("websocketDebuggerServerPort")
|
|
.SetStringValue(options.websocketDebuggerServerPort);
|
|
runtimeGameOptions.AddChild("electronRemoteRequirePath")
|
|
.SetStringValue(options.electronRemoteRequirePath);
|
|
if (options.isDevelopmentEnvironment) {
|
|
runtimeGameOptions.AddChild("environment").SetStringValue("dev");
|
|
}
|
|
if (!options.gdevelopResourceToken.empty()) {
|
|
runtimeGameOptions.AddChild("gdevelopResourceToken")
|
|
.SetStringValue(options.gdevelopResourceToken);
|
|
}
|
|
runtimeGameOptions.AddChild("allowAuthenticationUsingIframeForPreview")
|
|
.SetBoolValue(options.allowAuthenticationUsingIframeForPreview);
|
|
if (!options.playerId.empty() && !options.playerToken.empty()) {
|
|
runtimeGameOptions.AddChild("playerUsername")
|
|
.SetStringValue(options.playerUsername);
|
|
runtimeGameOptions.AddChild("playerId").SetStringValue(options.playerId);
|
|
runtimeGameOptions.AddChild("playerToken")
|
|
.SetStringValue(options.playerToken);
|
|
}
|
|
if (!options.crashReportUploadLevel.empty()) {
|
|
runtimeGameOptions.AddChild("crashReportUploadLevel")
|
|
.SetStringValue(options.crashReportUploadLevel);
|
|
}
|
|
if (!options.previewContext.empty()) {
|
|
runtimeGameOptions.AddChild("previewContext")
|
|
.SetStringValue(options.previewContext);
|
|
}
|
|
runtimeGameOptions.AddChild("gdevelopVersionWithHash")
|
|
.SetStringValue(options.gdevelopVersionWithHash);
|
|
if (!options.projectTemplateSlug.empty()) {
|
|
runtimeGameOptions.AddChild("projectTemplateSlug")
|
|
.SetStringValue(options.projectTemplateSlug);
|
|
}
|
|
if (!options.sourceGameId.empty()) {
|
|
runtimeGameOptions.AddChild("sourceGameId")
|
|
.SetStringValue(options.sourceGameId);
|
|
}
|
|
|
|
if (!options.captureOptions.IsEmpty()) {
|
|
auto &captureOptionsElement = runtimeGameOptions.AddChild("captureOptions");
|
|
const auto &screenshots = options.captureOptions.GetScreenshots();
|
|
if (!screenshots.empty()) {
|
|
auto &screenshotsElement = captureOptionsElement.AddChild("screenshots");
|
|
screenshotsElement.ConsiderAsArrayOf("screenshot");
|
|
for (const auto &screenshot : screenshots) {
|
|
screenshotsElement.AddChild("screenshot")
|
|
.SetIntAttribute("delayTimeInSeconds",
|
|
screenshot.GetDelayTimeInSeconds())
|
|
.SetStringAttribute("signedUrl", screenshot.GetSignedUrl())
|
|
.SetStringAttribute("publicUrl", screenshot.GetPublicUrl());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass in the options the list of scripts files - useful for hot-reloading.
|
|
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
|
|
scriptFilesElement.ConsiderAsArrayOf("scriptFile");
|
|
|
|
for (const auto &includeFile : includesFiles) {
|
|
auto hashIt = options.includeFileHashes.find(includeFile);
|
|
gd::String scriptSrc = GetExportedIncludeFilename(includeFile);
|
|
scriptFilesElement.AddChild("scriptFile")
|
|
.SetStringAttribute("path", scriptSrc)
|
|
.SetIntAttribute(
|
|
"hash",
|
|
hashIt != options.includeFileHashes.end() ? hashIt->second : 0);
|
|
}
|
|
|
|
// Export the project
|
|
ExportProjectData(fs,
|
|
exportedProject,
|
|
codeOutputDir + "/data.js",
|
|
runtimeGameOptions,
|
|
projectUsedResources,
|
|
scenesUsedResources);
|
|
includesFiles.push_back(codeOutputDir + "/data.js");
|
|
|
|
previousTime = LogTimeSpent("Project data export", previousTime);
|
|
|
|
// Copy all the dependencies and their source maps
|
|
ExportIncludesAndLibs(includesFiles, options.exportPath, true);
|
|
ExportIncludesAndLibs(resourcesFiles, options.exportPath, true);
|
|
|
|
// Create the index file
|
|
if (!ExportPixiIndexFile(exportedProject,
|
|
gdjsRoot + "/Runtime/index.html",
|
|
options.exportPath,
|
|
includesFiles,
|
|
options.nonRuntimeScriptsCacheBurst,
|
|
"gdjs.runtimeGameOptions"))
|
|
return false;
|
|
|
|
previousTime = LogTimeSpent("Include and libs export", previousTime);
|
|
return true;
|
|
}
|
|
|
|
gd::String ExporterHelper::ExportProjectData(
|
|
gd::AbstractFileSystem &fs,
|
|
gd::Project &project,
|
|
gd::String filename,
|
|
const gd::SerializerElement &runtimeGameOptions,
|
|
std::set<gd::String> &projectUsedResources,
|
|
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
|
|
fs.MkDir(fs.DirNameFrom(filename));
|
|
|
|
// Save the project to JSON
|
|
gd::SerializerElement rootElement;
|
|
project.SerializeTo(rootElement);
|
|
SerializeUsedResources(
|
|
rootElement, projectUsedResources, scenesUsedResources);
|
|
gd::String output =
|
|
"gdjs.projectData = " + gd::Serializer::ToJSON(rootElement) + ";\n" +
|
|
"gdjs.runtimeGameOptions = " +
|
|
gd::Serializer::ToJSON(runtimeGameOptions) + ";\n";
|
|
|
|
if (!fs.WriteToFile(filename, output)) return "Unable to write " + filename;
|
|
|
|
return "";
|
|
}
|
|
|
|
void ExporterHelper::SerializeUsedResources(
|
|
gd::SerializerElement &rootElement,
|
|
std::set<gd::String> &projectUsedResources,
|
|
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
|
|
auto serializeUsedResources =
|
|
[](gd::SerializerElement &element,
|
|
std::set<gd::String> &usedResources) -> void {
|
|
auto &resourcesElement = element.AddChild("usedResources");
|
|
resourcesElement.ConsiderAsArrayOf("resourceReference");
|
|
for (auto &resourceName : usedResources) {
|
|
auto &resourceElement = resourcesElement.AddChild("resourceReference");
|
|
resourceElement.SetAttribute("name", resourceName);
|
|
}
|
|
};
|
|
|
|
serializeUsedResources(rootElement, projectUsedResources);
|
|
|
|
auto &layoutsElement = rootElement.GetChild("layouts");
|
|
for (std::size_t layoutIndex = 0;
|
|
layoutIndex < layoutsElement.GetChildrenCount();
|
|
layoutIndex++) {
|
|
auto &layoutElement = layoutsElement.GetChild(layoutIndex);
|
|
const auto layoutName = layoutElement.GetStringAttribute("name");
|
|
|
|
auto &layoutUsedResources = scenesUsedResources[layoutName];
|
|
serializeUsedResources(layoutElement, layoutUsedResources);
|
|
}
|
|
}
|
|
|
|
bool ExporterHelper::ExportPixiIndexFile(
|
|
const gd::Project &project,
|
|
gd::String source,
|
|
gd::String exportDir,
|
|
const std::vector<gd::String> &includesFiles,
|
|
unsigned int nonRuntimeScriptsCacheBurst,
|
|
gd::String additionalSpec) {
|
|
gd::String str = fs.ReadFile(source);
|
|
|
|
// Generate the file
|
|
if (!CompleteIndexFile(str,
|
|
exportDir,
|
|
includesFiles,
|
|
nonRuntimeScriptsCacheBurst,
|
|
additionalSpec))
|
|
return false;
|
|
|
|
// Write the index.html file
|
|
if (!fs.WriteToFile(exportDir + "/index.html", str)) {
|
|
lastError = "Unable to write index file.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportCordovaFiles(const gd::Project &project,
|
|
gd::String exportDir,
|
|
std::set<gd::String> usedExtensions) {
|
|
auto &platformSpecificAssets = project.GetPlatformSpecificAssets();
|
|
auto &resourceManager = project.GetResourcesManager();
|
|
auto getIconFilename = [&resourceManager, &platformSpecificAssets](
|
|
const gd::String &platform,
|
|
const gd::String &name) {
|
|
const gd::String &file =
|
|
resourceManager.GetResource(platformSpecificAssets.Get(platform, name))
|
|
.GetFile();
|
|
return file.empty() ? "" : "www/" + file;
|
|
};
|
|
|
|
auto makeIconsAndroid = [&getIconFilename]() {
|
|
std::vector<std::pair<gd::String, gd::String>> sizes = {{"36", "ldpi"},
|
|
{"48", "mdpi"},
|
|
{"72", "hdpi"},
|
|
{"96", "xhdpi"},
|
|
{"144", "xxhdpi"},
|
|
{"192", "xxxhdpi"}};
|
|
|
|
gd::String output;
|
|
for (auto &size : sizes) {
|
|
gd::String filename = getIconFilename("android", "icon-" + size.first);
|
|
output += !filename.empty() ? ("<icon src=\"" + filename +
|
|
"\" density=\"" + size.second + "\" />\n")
|
|
: "";
|
|
}
|
|
|
|
// Splashscreen icon for Android 12+.
|
|
gd::String splashScreenIconFilename =
|
|
getIconFilename("android", "windowSplashScreenAnimatedIcon");
|
|
if (!splashScreenIconFilename.empty())
|
|
output +=
|
|
"<preference name=\"AndroidWindowSplashScreenAnimatedIcon\" "
|
|
"value=\"" +
|
|
splashScreenIconFilename + "\" />\n";
|
|
|
|
// Splashscreen "branding" image for Android 12+.
|
|
gd::String splashScreenBrandingImageFilename =
|
|
getIconFilename("android", "windowSplashScreenBrandingImage");
|
|
if (!splashScreenBrandingImageFilename.empty())
|
|
output +=
|
|
"<preference name=\"AndroidWindowSplashScreenBrandingImage\" "
|
|
"value=\"" +
|
|
splashScreenBrandingImageFilename + "\" />\n";
|
|
|
|
return output;
|
|
};
|
|
|
|
auto makeIconsIos = [&getIconFilename]() {
|
|
std::vector<gd::String> sizes = {
|
|
"180", "60", "120", "76", "152", "40", "80", "57", "114", "72",
|
|
"144", "167", "29", "58", "87", "50", "20", "100", "167", "1024"};
|
|
|
|
gd::String output;
|
|
for (auto &size : sizes) {
|
|
gd::String filename = getIconFilename("ios", "icon-" + size);
|
|
output += !filename.empty() ? ("<icon src=\"" + filename + "\" width=\"" +
|
|
size + "\" height=\"" + size + "\" />\n")
|
|
: "";
|
|
}
|
|
|
|
return output;
|
|
};
|
|
|
|
auto makeProjectNameXcodeSafe = [](const gd::String &projectName) {
|
|
// Avoid App Store Connect STATE_ERROR.VALIDATION_ERROR.90121 error, when
|
|
// "CFBundleExecutable Info.plist key contains [...] any of the following
|
|
// unsupported characters: \ [ ] { } ( ) + *".
|
|
|
|
// Remove \ [ ] { } ( ) + * from the project name.
|
|
return projectName.FindAndReplace("\\", "")
|
|
.FindAndReplace("[", "")
|
|
.FindAndReplace("]", "")
|
|
.FindAndReplace("{", "")
|
|
.FindAndReplace("}", "")
|
|
.FindAndReplace("(", "")
|
|
.FindAndReplace(")", "")
|
|
.FindAndReplace("+", "")
|
|
.FindAndReplace("*", "");
|
|
};
|
|
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Cordova/config.xml")
|
|
.FindAndReplace("GDJS_PROJECTNAME",
|
|
gd::Serializer::ToEscapedXMLString(
|
|
makeProjectNameXcodeSafe(project.GetName())))
|
|
.FindAndReplace(
|
|
"GDJS_PACKAGENAME",
|
|
gd::Serializer::ToEscapedXMLString(project.GetPackageName()))
|
|
.FindAndReplace("GDJS_PROJECTVERSION", project.GetVersion())
|
|
.FindAndReplace("<!-- GDJS_ICONS_ANDROID -->", makeIconsAndroid())
|
|
.FindAndReplace("<!-- GDJS_ICONS_IOS -->", makeIconsIos());
|
|
|
|
gd::String plugins = "";
|
|
auto dependenciesAndExtensions =
|
|
gd::ExportedDependencyResolver::GetDependenciesFor(
|
|
project, usedExtensions, "cordova");
|
|
for (auto &dependencyAndExtension : dependenciesAndExtensions) {
|
|
const auto &dependency = dependencyAndExtension.GetDependency();
|
|
|
|
gd::String plugin;
|
|
plugin += "<plugin name=\"" +
|
|
gd::Serializer::ToEscapedXMLString(dependency.GetExportName());
|
|
if (dependency.GetVersion() != "") {
|
|
plugin += "\" spec=\"" +
|
|
gd::Serializer::ToEscapedXMLString(dependency.GetVersion());
|
|
}
|
|
plugin += "\">\n";
|
|
|
|
auto extraSettingValues = gd::ExportedDependencyResolver::
|
|
GetExtensionDependencyExtraSettingValues(project,
|
|
dependencyAndExtension);
|
|
|
|
// For Cordova, all settings are considered a plugin variable.
|
|
for (auto &extraSetting : extraSettingValues) {
|
|
plugin += " <variable name=\"" +
|
|
gd::Serializer::ToEscapedXMLString(extraSetting.first) +
|
|
"\" value=\"" +
|
|
gd::Serializer::ToEscapedXMLString(extraSetting.second) +
|
|
"\" />\n";
|
|
}
|
|
|
|
plugin += "</plugin>";
|
|
|
|
plugins += plugin;
|
|
}
|
|
|
|
// TODO: migrate the plugins to the package.json
|
|
str =
|
|
str.FindAndReplace("<!-- GDJS_EXTENSION_CORDOVA_DEPENDENCY -->", plugins);
|
|
|
|
if (!fs.WriteToFile(exportDir + "/config.xml", str)) {
|
|
lastError = "Unable to write Cordova config.xml file.";
|
|
return false;
|
|
}
|
|
|
|
gd::String jsonName =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetName()));
|
|
gd::String jsonAuthor =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetAuthor()));
|
|
gd::String jsonVersion =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetVersion()));
|
|
gd::String jsonMangledName = gd::Serializer::ToJSON(
|
|
gd::SerializerElement(gd::SceneNameMangler::Get()
|
|
->GetMangledSceneName(project.GetName())
|
|
.LowerCase()
|
|
.FindAndReplace(" ", "-")));
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Cordova/package.json")
|
|
.FindAndReplace("\"GDJS_GAME_NAME\"", jsonName)
|
|
.FindAndReplace("\"GDJS_GAME_AUTHOR\"", jsonAuthor)
|
|
.FindAndReplace("\"GDJS_GAME_VERSION\"", jsonVersion)
|
|
.FindAndReplace("\"GDJS_GAME_MANGLED_NAME\"", jsonMangledName);
|
|
|
|
if (!fs.WriteToFile(exportDir + "/package.json", str)) {
|
|
lastError = "Unable to write Cordova package.json file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Cordova/www/LICENSE.GDevelop.txt");
|
|
|
|
if (!fs.WriteToFile(exportDir + "/www/LICENSE.GDevelop.txt", str)) {
|
|
lastError = "Unable to write Cordova LICENSE.GDevelop.txt file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportFacebookInstantGamesFiles(const gd::Project &project,
|
|
gd::String exportDir) {
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot +
|
|
"/Runtime/FacebookInstantGames/fbapp-config.json")
|
|
.FindAndReplace("\"GDJS_ORIENTATION\"",
|
|
project.GetOrientation() == "portrait"
|
|
? "\"PORTRAIT\""
|
|
: "\"LANDSCAPE\"");
|
|
|
|
if (!fs.WriteToFile(exportDir + "/fbapp-config.json", str)) {
|
|
lastError =
|
|
"Unable to write Facebook Instant Games fbapp-config.json file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportHtml5Files(const gd::Project &project,
|
|
gd::String exportDir) {
|
|
if (!fs.WriteToFile(exportDir + "/manifest.webmanifest",
|
|
GenerateWebManifest(project))) {
|
|
lastError = "Unable to export WebManifest.";
|
|
return false;
|
|
}
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Electron/LICENSE.GDevelop.txt");
|
|
|
|
if (!fs.WriteToFile(exportDir + "/LICENSE.GDevelop.txt", str)) {
|
|
lastError = "Unable to write LICENSE.GDevelop.txt file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportElectronFiles(const gd::Project &project,
|
|
gd::String exportDir,
|
|
std::set<gd::String> usedExtensions) {
|
|
gd::String jsonName =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetName()));
|
|
gd::String jsonPackageName =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetPackageName()));
|
|
gd::String jsonAuthor =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetAuthor()));
|
|
gd::String jsonVersion =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetVersion()));
|
|
gd::String jsonMangledName = gd::Serializer::ToJSON(
|
|
gd::SerializerElement(gd::SceneNameMangler::Get()
|
|
->GetMangledSceneName(project.GetName())
|
|
.LowerCase()
|
|
.FindAndReplace(" ", "-")));
|
|
// It's important to clean the project name from special characters,
|
|
// otherwise Windows executable may be corrupted when electron builds it.
|
|
gd::String jsonCleanedName = gd::Serializer::ToJSON(
|
|
gd::SerializerElement(CleanProjectName(project.GetName())));
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Electron/package.json")
|
|
.FindAndReplace("\"GDJS_GAME_NAME\"", jsonCleanedName)
|
|
.FindAndReplace("\"GDJS_GAME_PACKAGE_NAME\"", jsonPackageName)
|
|
.FindAndReplace("\"GDJS_GAME_AUTHOR\"", jsonAuthor)
|
|
.FindAndReplace("\"GDJS_GAME_VERSION\"", jsonVersion)
|
|
.FindAndReplace("\"GDJS_GAME_MANGLED_NAME\"", jsonMangledName);
|
|
|
|
gd::String packages = "";
|
|
|
|
auto dependenciesAndExtensions =
|
|
gd::ExportedDependencyResolver::GetDependenciesFor(
|
|
project, usedExtensions, "npm");
|
|
for (auto &dependencyAndExtension : dependenciesAndExtensions) {
|
|
const auto &dependency = dependencyAndExtension.GetDependency();
|
|
if (dependency.GetVersion() == "") {
|
|
gd::LogError(
|
|
"Latest Version not available for NPM dependencies, "
|
|
"dependency " +
|
|
dependency.GetName() +
|
|
" is not exported. Please specify a version when calling "
|
|
"addDependency.");
|
|
continue;
|
|
}
|
|
|
|
// For Electron, extra settings of dependencies are ignored.
|
|
packages += "\n\t\"" + dependency.GetExportName() + "\": \"" +
|
|
dependency.GetVersion() + "\",";
|
|
}
|
|
|
|
str = str.FindAndReplace("\"GDJS_EXTENSION_NPM_DEPENDENCY\": \"0\",",
|
|
packages);
|
|
|
|
if (!fs.WriteToFile(exportDir + "/package.json", str)) {
|
|
lastError = "Unable to write Electron package.json file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Electron/main.js")
|
|
.FindAndReplace(
|
|
"800 /*GDJS_WINDOW_WIDTH*/",
|
|
gd::String::From<int>(project.GetGameResolutionWidth()))
|
|
.FindAndReplace(
|
|
"600 /*GDJS_WINDOW_HEIGHT*/",
|
|
gd::String::From<int>(project.GetGameResolutionHeight()))
|
|
.FindAndReplace("\"GDJS_GAME_NAME\"", jsonName);
|
|
|
|
if (!fs.WriteToFile(exportDir + "/main.js", str)) {
|
|
lastError = "Unable to write Electron main.js file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
{
|
|
gd::String str =
|
|
fs.ReadFile(gdjsRoot + "/Runtime/Electron/LICENSE.GDevelop.txt");
|
|
|
|
if (!fs.WriteToFile(exportDir + "/LICENSE.GDevelop.txt", str)) {
|
|
lastError = "Unable to write Electron LICENSE.GDevelop.txt file.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportBuildResourcesElectronFiles(
|
|
const gd::Project &project, gd::String exportDir) {
|
|
auto &platformSpecificAssets = project.GetPlatformSpecificAssets();
|
|
auto &resourceManager = project.GetResourcesManager();
|
|
|
|
gd::String iconFilename =
|
|
resourceManager
|
|
.GetResource(platformSpecificAssets.Get("desktop", "icon-512"))
|
|
.GetFile();
|
|
|
|
auto projectDirectory = gd::AbstractFileSystem::NormalizeSeparator(
|
|
fs.DirNameFrom(project.GetProjectFile()));
|
|
fs.MakeAbsolute(iconFilename, projectDirectory);
|
|
fs.MkDir(exportDir + "/buildResources");
|
|
if (fs.FileExists(iconFilename)) {
|
|
fs.CopyFile(iconFilename, exportDir + "/buildResources/icon.png");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::CompleteIndexFile(
|
|
gd::String &str,
|
|
gd::String exportDir,
|
|
const std::vector<gd::String> &includesFiles,
|
|
unsigned int nonRuntimeScriptsCacheBurst,
|
|
gd::String additionalSpec) {
|
|
if (additionalSpec.empty()) additionalSpec = "{}";
|
|
|
|
gd::String codeFilesIncludes;
|
|
for (auto &include : includesFiles) {
|
|
gd::String scriptSrc =
|
|
GetExportedIncludeFilename(include, nonRuntimeScriptsCacheBurst);
|
|
|
|
// Sanity check if the file exists - if not skip it to avoid
|
|
// including it in the list of scripts.
|
|
gd::String absoluteFilename = scriptSrc;
|
|
fs.MakeAbsolute(absoluteFilename, exportDir);
|
|
if (!fs.FileExists(absoluteFilename)) {
|
|
std::cout << "Warning: Unable to find " << absoluteFilename << "."
|
|
<< std::endl;
|
|
continue;
|
|
}
|
|
|
|
codeFilesIncludes += "\t<script src=\"" + scriptSrc +
|
|
"\" crossorigin=\"anonymous\"></script>\n";
|
|
}
|
|
|
|
str = str.FindAndReplace("/* GDJS_CUSTOM_STYLE */", "")
|
|
.FindAndReplace("<!-- GDJS_CUSTOM_HTML -->", "")
|
|
.FindAndReplace("<!-- GDJS_CODE_FILES -->", codeFilesIncludes)
|
|
.FindAndReplace("{}/*GDJS_ADDITIONAL_SPEC*/", additionalSpec);
|
|
|
|
return true;
|
|
}
|
|
|
|
void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
|
bool pixiInThreeRenderers,
|
|
bool includeWebsocketDebuggerClient,
|
|
bool includeWindowMessageDebuggerClient,
|
|
bool includeMinimalDebuggerClient,
|
|
bool includeCaptureManager,
|
|
gd::String gdevelopLogoStyle,
|
|
std::vector<gd::String> &includesFiles) {
|
|
// First, do not forget common includes (they must be included before events
|
|
// generated code files).
|
|
InsertUnique(includesFiles, "libs/jshashtable.js");
|
|
InsertUnique(includesFiles, "logger.js");
|
|
InsertUnique(includesFiles, "gd.js");
|
|
InsertUnique(includesFiles, "libs/rbush.js");
|
|
InsertUnique(includesFiles, "AsyncTasksManager.js");
|
|
InsertUnique(includesFiles, "inputmanager.js");
|
|
InsertUnique(includesFiles, "jsonmanager.js");
|
|
InsertUnique(includesFiles, "Model3DManager.js");
|
|
InsertUnique(includesFiles, "ResourceLoader.js");
|
|
InsertUnique(includesFiles, "ResourceCache.js");
|
|
InsertUnique(includesFiles, "timemanager.js");
|
|
InsertUnique(includesFiles, "polygon.js");
|
|
InsertUnique(includesFiles, "runtimeobject.js");
|
|
InsertUnique(includesFiles, "profiler.js");
|
|
InsertUnique(includesFiles, "RuntimeInstanceContainer.js");
|
|
InsertUnique(includesFiles, "runtimescene.js");
|
|
InsertUnique(includesFiles, "scenestack.js");
|
|
InsertUnique(includesFiles, "force.js");
|
|
InsertUnique(includesFiles, "RuntimeLayer.js");
|
|
InsertUnique(includesFiles, "layer.js");
|
|
InsertUnique(includesFiles, "RuntimeCustomObjectLayer.js");
|
|
InsertUnique(includesFiles, "timer.js");
|
|
InsertUnique(includesFiles, "runtimewatermark.js");
|
|
InsertUnique(includesFiles, "runtimegame.js");
|
|
InsertUnique(includesFiles, "variable.js");
|
|
InsertUnique(includesFiles, "variablescontainer.js");
|
|
InsertUnique(includesFiles, "oncetriggers.js");
|
|
InsertUnique(includesFiles, "runtimebehavior.js");
|
|
InsertUnique(includesFiles, "SpriteAnimator.js");
|
|
InsertUnique(includesFiles, "spriteruntimeobject.js");
|
|
InsertUnique(includesFiles, "affinetransformation.js");
|
|
InsertUnique(includesFiles, "CustomRuntimeObjectInstanceContainer.js");
|
|
InsertUnique(includesFiles, "CustomRuntimeObject.js");
|
|
InsertUnique(includesFiles, "CustomRuntimeObject2D.js");
|
|
|
|
// Common includes for events only.
|
|
InsertUnique(includesFiles, "events-tools/commontools.js");
|
|
InsertUnique(includesFiles, "events-tools/variabletools.js");
|
|
InsertUnique(includesFiles, "events-tools/runtimescenetools.js");
|
|
InsertUnique(includesFiles, "events-tools/inputtools.js");
|
|
InsertUnique(includesFiles, "events-tools/objecttools.js");
|
|
InsertUnique(includesFiles, "events-tools/cameratools.js");
|
|
InsertUnique(includesFiles, "events-tools/soundtools.js");
|
|
InsertUnique(includesFiles, "events-tools/storagetools.js");
|
|
InsertUnique(includesFiles, "events-tools/stringtools.js");
|
|
InsertUnique(includesFiles, "events-tools/windowtools.js");
|
|
InsertUnique(includesFiles, "events-tools/networktools.js");
|
|
|
|
if (gdevelopLogoStyle == "dark") {
|
|
InsertUnique(includesFiles, "splash/gd-logo-dark.js");
|
|
} else if (gdevelopLogoStyle == "dark-colored") {
|
|
InsertUnique(includesFiles, "splash/gd-logo-dark-colored.js");
|
|
} else if (gdevelopLogoStyle == "light-colored") {
|
|
InsertUnique(includesFiles, "splash/gd-logo-light-colored.js");
|
|
} else {
|
|
InsertUnique(includesFiles, "splash/gd-logo-light.js");
|
|
}
|
|
|
|
if (includeWebsocketDebuggerClient || includeWindowMessageDebuggerClient) {
|
|
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
|
|
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
|
|
InsertUnique(includesFiles, "debugger-client/InGameDebugger.js");
|
|
}
|
|
if (includeWebsocketDebuggerClient) {
|
|
InsertUnique(includesFiles, "debugger-client/websocket-debugger-client.js");
|
|
}
|
|
if (includeWindowMessageDebuggerClient) {
|
|
InsertUnique(includesFiles,
|
|
"debugger-client/window-message-debugger-client.js");
|
|
}
|
|
if (includeMinimalDebuggerClient) {
|
|
InsertUnique(includesFiles, "debugger-client/minimal-debugger-client.js");
|
|
}
|
|
|
|
if (pixiInThreeRenderers) {
|
|
InsertUnique(includesFiles, "pixi-renderers/three.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/ThreeAddons.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/draco/gltf/draco_decoder.wasm");
|
|
InsertUnique(includesFiles,
|
|
"pixi-renderers/draco/gltf/draco_wasm_wrapper.js");
|
|
}
|
|
if (pixiRenderers) {
|
|
InsertUnique(includesFiles, "pixi-renderers/pixi.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/pixi-filters-tools.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/runtimegame-pixi-renderer.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/runtimescene-pixi-renderer.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/layer-pixi-renderer.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/pixi-image-manager.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/pixi-bitmapfont-manager.js");
|
|
InsertUnique(includesFiles,
|
|
"pixi-renderers/spriteruntimeobject-pixi-renderer.js");
|
|
InsertUnique(includesFiles,
|
|
"pixi-renderers/CustomRuntimeObject2DPixiRenderer.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/DebuggerPixiRenderer.js");
|
|
InsertUnique(includesFiles,
|
|
"pixi-renderers/loadingscreen-pixi-renderer.js");
|
|
InsertUnique(includesFiles, "pixi-renderers/pixi-effects-manager.js");
|
|
InsertUnique(includesFiles, "howler-sound-manager/howler.min.js");
|
|
InsertUnique(includesFiles, "howler-sound-manager/howler-sound-manager.js");
|
|
InsertUnique(includesFiles,
|
|
"fontfaceobserver-font-manager/fontfaceobserver.js");
|
|
InsertUnique(
|
|
includesFiles,
|
|
"fontfaceobserver-font-manager/fontfaceobserver-font-manager.js");
|
|
}
|
|
if (pixiInThreeRenderers) {
|
|
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3D.js");
|
|
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3DRenderer.js");
|
|
InsertUnique(includesFiles, "Extensions/3D/CustomRuntimeObject3D.js");
|
|
InsertUnique(includesFiles,
|
|
"Extensions/3D/CustomRuntimeObject3DRenderer.js");
|
|
}
|
|
if (includeCaptureManager) {
|
|
InsertUnique(includesFiles, "capturemanager.js");
|
|
}
|
|
}
|
|
|
|
void ExporterHelper::RemoveIncludes(bool pixiRenderers,
|
|
std::vector<gd::String> &includesFiles) {
|
|
if (pixiRenderers) {
|
|
for (size_t i = 0; i < includesFiles.size();) {
|
|
const gd::String &includeFile = includesFiles[i];
|
|
if (includeFile.find("pixi-renderer") != gd::String::npos ||
|
|
includeFile.find("pixi-filter") != gd::String::npos)
|
|
includesFiles.erase(includesFiles.begin() + i);
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ExporterHelper::ExportEffectIncludes(
|
|
gd::Project &project, std::vector<gd::String> &includesFiles) {
|
|
std::set<gd::String> effectIncludes;
|
|
|
|
gd::EffectsCodeGenerator::GenerateEffectsIncludeFiles(
|
|
project.GetCurrentPlatform(), project, effectIncludes);
|
|
|
|
for (auto &include : effectIncludes) InsertUnique(includesFiles, include);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportEventsCode(
|
|
const gd::Project &project,
|
|
gd::String outputDir,
|
|
std::vector<gd::String> &includesFiles,
|
|
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport,
|
|
bool exportForPreview) {
|
|
fs.MkDir(outputDir);
|
|
|
|
for (std::size_t i = 0; i < project.GetLayoutsCount(); ++i) {
|
|
std::set<gd::String> eventsIncludes;
|
|
const gd::Layout &layout = project.GetLayout(i);
|
|
|
|
auto &diagnosticReport =
|
|
wholeProjectDiagnosticReport.AddNewDiagnosticReportForScene(
|
|
layout.GetName());
|
|
LayoutCodeGenerator layoutCodeGenerator(project);
|
|
gd::String eventsOutput = layoutCodeGenerator.GenerateLayoutCompleteCode(
|
|
layout, eventsIncludes, diagnosticReport, !exportForPreview);
|
|
gd::String filename =
|
|
outputDir + "/" + "code" + gd::String::From(i) + ".js";
|
|
|
|
// Export the code
|
|
if (fs.WriteToFile(filename, eventsOutput)) {
|
|
for (auto &include : eventsIncludes) InsertUnique(includesFiles, include);
|
|
|
|
InsertUnique(includesFiles, filename);
|
|
} else {
|
|
lastError = _("Unable to write ") + filename;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ExporterHelper::ExportExternalSourceFiles(
|
|
const gd::Project &project,
|
|
gd::String outputDir,
|
|
std::vector<gd::String> &includesFiles) {
|
|
const auto &allFiles = project.GetAllSourceFiles();
|
|
for (std::size_t i = 0; i < allFiles.size(); ++i) {
|
|
if (!allFiles[i]) continue;
|
|
if (allFiles[i]->GetLanguage() != "Javascript") continue;
|
|
|
|
gd::SourceFile &file = *allFiles[i];
|
|
|
|
gd::String filename = file.GetFileName();
|
|
fs.MakeAbsolute(filename, fs.DirNameFrom(project.GetProjectFile()));
|
|
gd::String outFilename = "ext-code" + gd::String::From(i) + ".js";
|
|
if (!fs.CopyFile(filename, outputDir + outFilename))
|
|
gd::LogWarning(_("Could not copy external file") + filename);
|
|
|
|
InsertUnique(includesFiles, outputDir + outFilename);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
gd::String ExporterHelper::GetExportedIncludeFilename(
|
|
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst) {
|
|
auto addSearchParameterToUrl = [](const gd::String &url,
|
|
const gd::String &urlEncodedParameterName,
|
|
const gd::String &urlEncodedValue) {
|
|
gd::String separator = url.find("?") == gd::String::npos ? "?" : "&";
|
|
return url + separator + urlEncodedParameterName + "=" + urlEncodedValue;
|
|
};
|
|
|
|
if (!fs.IsAbsolute(include)) {
|
|
// By convention, an include file that is relative is relative to
|
|
// the "<GDJS Root>/Runtime" folder, and will have the same relative
|
|
// path when exported.
|
|
|
|
// We still do this seemingly useless relative to absolute to relative
|
|
// conversion, because some filesystems are using a URL for gdjsRoot, and
|
|
// will convert the relative include to an absolute URL.
|
|
gd::String relativeInclude = gdjsRoot + "/Runtime/" + include;
|
|
fs.MakeRelative(relativeInclude, gdjsRoot + "/Runtime/");
|
|
return relativeInclude;
|
|
} else {
|
|
// Note: all the code generated from events are generated in another
|
|
// folder and fall in this case:
|
|
gd::String resolvedInclude = fs.FileNameFrom(include);
|
|
|
|
if (nonRuntimeScriptsCacheBurst == 0) {
|
|
return resolvedInclude;
|
|
}
|
|
|
|
// Add the parameter to force the browser to reload the code - useful
|
|
// for cases where the browser is caching files that are getting
|
|
// overwritten.
|
|
return addSearchParameterToUrl(
|
|
resolvedInclude,
|
|
"gdCacheBurst",
|
|
gd::String::From(nonRuntimeScriptsCacheBurst));
|
|
}
|
|
}
|
|
|
|
bool ExporterHelper::ExportIncludesAndLibs(
|
|
const std::vector<gd::String> &includesFiles,
|
|
gd::String exportDir,
|
|
bool exportSourceMaps) {
|
|
for (auto &include : includesFiles) {
|
|
if (!fs.IsAbsolute(include)) {
|
|
// By convention, an include file that is relative is relative to
|
|
// the "<GDJS Root>/Runtime" folder, and will have the same relative
|
|
// path when exported.
|
|
gd::String source = gdjsRoot + "/Runtime/" + include;
|
|
if (fs.FileExists(source)) {
|
|
gd::String path = fs.DirNameFrom(exportDir + "/" + include);
|
|
if (!fs.DirExists(path)) fs.MkDir(path);
|
|
|
|
fs.CopyFile(source, exportDir + "/" + include);
|
|
|
|
gd::String sourceMap = source + ".map";
|
|
// Copy source map if present
|
|
if (exportSourceMaps && fs.FileExists(sourceMap)) {
|
|
fs.CopyFile(sourceMap, exportDir + "/" + include + ".map");
|
|
}
|
|
} else {
|
|
std::cout << "Could not find GDJS include file " << include
|
|
<< std::endl;
|
|
}
|
|
} else {
|
|
// Note: all the code generated from events are generated in another
|
|
// folder and fall in this case:
|
|
if (fs.FileExists(include)) {
|
|
fs.CopyFile(include, exportDir + "/" + fs.FileNameFrom(include));
|
|
} else {
|
|
std::cout << "Could not find include file " << include << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ExporterHelper::ExportResources(gd::AbstractFileSystem &fs,
|
|
gd::Project &project,
|
|
gd::String exportDir) {
|
|
gd::ProjectResourcesCopier::CopyAllResourcesTo(
|
|
project, fs, exportDir, true, false, false);
|
|
}
|
|
|
|
void ExporterHelper::AddDeprecatedFontFilesToFontResources(
|
|
gd::AbstractFileSystem &fs,
|
|
gd::ResourcesManager &resourcesManager,
|
|
const gd::String &exportDir,
|
|
gd::String urlPrefix) {
|
|
// Compatibility with GD <= 5.0-beta56
|
|
//
|
|
// Before, fonts were detected by scanning the export folder for .TTF files.
|
|
// Text Object (or anything using a font) was just declaring the font filename
|
|
// as a file (using ArbitraryResourceWorker::ExposeFile) for export.
|
|
//
|
|
// To still support this, the time everything is migrated to using font
|
|
// resources, we manually declare font resources for each ".TTF" file, using
|
|
// the name of the file as the resource name.
|
|
std::vector<gd::String> ttfFiles = fs.ReadDir(exportDir, ".TTF");
|
|
for (std::size_t i = 0; i < ttfFiles.size(); ++i) {
|
|
gd::String relativeFile = ttfFiles[i];
|
|
fs.MakeRelative(relativeFile, exportDir);
|
|
|
|
// Create a resource named like the file (to emulate the old behavior).
|
|
gd::FontResource fontResource;
|
|
fontResource.SetName(relativeFile);
|
|
fontResource.SetFile(urlPrefix + relativeFile);
|
|
|
|
// Note that if a resource with this name already exists, it won't be
|
|
// overwritten - which is expected.
|
|
resourcesManager.AddResource(fontResource);
|
|
}
|
|
// end of compatibility code
|
|
}
|
|
|
|
const std::array<int, 20> IOS_ICONS_SIZES = {
|
|
180, 60, 120, 76, 152, 40, 80, 57, 114, 72,
|
|
144, 167, 29, 58, 87, 50, 20, 100, 167, 1024,
|
|
};
|
|
const std::array<int, 6> ANDROID_ICONS_SIZES = {36, 48, 72, 96, 144, 192};
|
|
|
|
const gd::String ExporterHelper::GenerateWebManifest(
|
|
const gd::Project &project) {
|
|
const gd::String &orientation = project.GetOrientation();
|
|
gd::String icons = "[";
|
|
|
|
{
|
|
std::map<int, gd::String> resourcesForSizes;
|
|
const auto getFileNameForIcon = [&project](const gd::String &platform,
|
|
const int size) {
|
|
const gd::String iconName = "icon-" + gd::String::From(size);
|
|
return project.GetPlatformSpecificAssets().Has(platform, iconName)
|
|
? project.GetResourcesManager()
|
|
.GetResource(project.GetPlatformSpecificAssets().Get(
|
|
platform, iconName))
|
|
.GetFile()
|
|
: "";
|
|
};
|
|
|
|
for (const int size : IOS_ICONS_SIZES) {
|
|
const auto iconFile = getFileNameForIcon("ios", size);
|
|
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
|
|
};
|
|
|
|
for (const int size : ANDROID_ICONS_SIZES) {
|
|
const auto iconFile = getFileNameForIcon("android", size);
|
|
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
|
|
};
|
|
|
|
const auto desktopIconFile = getFileNameForIcon("desktop", 512);
|
|
if (!desktopIconFile.empty()) resourcesForSizes[512] = desktopIconFile;
|
|
|
|
for (const auto &sizeAndFile : resourcesForSizes) {
|
|
icons +=
|
|
gd::String(R"({
|
|
"src": "{FILE}",
|
|
"sizes": "{SIZE}x{SIZE}"
|
|
},)")
|
|
.FindAndReplace("{SIZE}", gd::String::From(sizeAndFile.first))
|
|
.FindAndReplace("{FILE}", sizeAndFile.second);
|
|
}
|
|
}
|
|
|
|
icons = icons.RightTrim(",") + "]";
|
|
|
|
gd::String jsonName =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetName()));
|
|
gd::String jsonPackageName =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetPackageName()));
|
|
gd::String jsonDescription =
|
|
gd::Serializer::ToJSON(gd::SerializerElement(project.GetDescription()));
|
|
|
|
return gd::String(R"webmanifest({
|
|
"name": {NAME},
|
|
"short_name": {NAME},
|
|
"id": {PACKAGE_ID},
|
|
"description": {DESCRIPTION},
|
|
"orientation": "{ORIENTATION}",
|
|
"start_url": "./index.html",
|
|
"display": "standalone",
|
|
"background_color": "black",
|
|
"categories": ["games", "entertainment"],
|
|
"icons": {ICONS}
|
|
})webmanifest")
|
|
.FindAndReplace("{NAME}", jsonName)
|
|
.FindAndReplace("{PACKAGE_ID}", jsonPackageName)
|
|
.FindAndReplace("{DESCRIPTION}", jsonDescription)
|
|
.FindAndReplace("{ORIENTATION}",
|
|
orientation == "default" ? "any" : orientation)
|
|
.FindAndReplace("{ICONS}", icons);
|
|
};
|
|
|
|
} // namespace gdjs
|