Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take automatic game screenshots in Quick Customization #7116

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Core/GDCore/IDE/CaptureOptions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival ([email protected]). All rights
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Copyright 2008-2016 Florian Rival ([email protected]). All rights
* Copyright 2008-2024 Florian Rival ([email protected]). All rights

* reserved. This project is released under the MIT License.
*/
#include "GDCore/IDE/CaptureOptions.h"

#include "GDCore/String.h"

using namespace std;

namespace gd {

Screenshot::Screenshot() {}

CaptureOptions::CaptureOptions() {}

} // namespace gd
50 changes: 50 additions & 0 deletions Core/GDCore/IDE/CaptureOptions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#pragma once
#include <memory>
#include <vector>

#include "GDCore/String.h"

namespace gd {

class GD_CORE_API Screenshot {
public:
Screenshot();
virtual ~Screenshot() {};

void SetDelayTimeInSeconds(int delayTimeInMs_) {
delayTimeInMs = delayTimeInMs_;
}
const long GetDelayTimeInSeconds() const { return delayTimeInMs; }
ClementPasteau marked this conversation as resolved.
Show resolved Hide resolved

void SetSignedUrl(const gd::String& signedUrl_) { signedUrl = signedUrl_; }
const gd::String& GetSignedUrl() const { return signedUrl; }

void SetPublicUrl(const gd::String& publicUrl_) { publicUrl = publicUrl_; }
const gd::String& GetPublicUrl() const { return publicUrl; }

private:
int delayTimeInMs = 0;
gd::String signedUrl;
gd::String publicUrl;
};

class GD_CORE_API CaptureOptions {
public:
CaptureOptions();
virtual ~CaptureOptions() {};

bool IsEmpty() const { return screenshots.empty(); }

void AddScreenshot(const Screenshot& screenshot) {
screenshots.push_back(screenshot);
}

const std::vector<Screenshot>& GetScreenshots() const { return screenshots; }

void ClearScreenshots() { screenshots.clear(); }

private:
std::vector<Screenshot> screenshots;
};

} // namespace gd
21 changes: 14 additions & 7 deletions GDJS/GDJS/IDE/Exporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
/*includeWebsocketDebuggerClient=*/false,
/*includeWindowMessageDebuggerClient=*/false,
/*includeMinimalDebuggerClient=*/false,
/*includeCaptureManager*/ false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);

Expand All @@ -119,8 +120,11 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
helper.ExportEffectIncludes(exportedProject, includesFiles);

// Export events
if (!helper.ExportEventsCode(exportedProject, codeOutputDir, includesFiles,
wholeProjectDiagnosticReport, false)) {
if (!helper.ExportEventsCode(exportedProject,
codeOutputDir,
includesFiles,
wholeProjectDiagnosticReport,
false)) {
gd::LogError(_("Error during exporting! Unable to export events:\n") +
lastError);
return false;
Expand All @@ -139,11 +143,11 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
gd::SceneResourcesFinder::FindProjectResources(exportedProject);
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < exportedProject.GetLayoutsCount(); layoutIndex++) {
layoutIndex < exportedProject.GetLayoutsCount();
layoutIndex++) {
auto &layout = exportedProject.GetLayout(layoutIndex);
scenesUsedResources[layout.GetName()] =
gd::SceneResourcesFinder::FindSceneResources(exportedProject,
layout);
gd::SceneResourcesFinder::FindSceneResources(exportedProject, layout);
}

// Strip the project (*after* generating events as the events may use
Expand All @@ -152,8 +156,11 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {

//...and export it
gd::SerializerElement noRuntimeGameOptions;
helper.ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
noRuntimeGameOptions, projectUsedResources,
helper.ExportProjectData(fs,
exportedProject,
codeOutputDir + "/data.js",
noRuntimeGameOptions,
projectUsedResources,
scenesUsedResources);
includesFiles.push_back(codeOutputDir + "/data.js");

Expand Down
23 changes: 23 additions & 0 deletions GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#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"
Expand Down Expand Up @@ -156,6 +157,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
options.useWindowMessageDebuggerClient,
/*includeMinimalDebuggerClient=*/
options.useMinimalDebuggerClient,
/*includeCaptureManager=*/
!options.captureOptions.IsEmpty(),
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);

Expand Down Expand Up @@ -269,6 +272,22 @@ bool ExporterHelper::ExportProjectForPixiPreview(
.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");
Expand Down Expand Up @@ -756,6 +775,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
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
Expand Down Expand Up @@ -872,6 +892,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles,
"Extensions/3D/CustomRuntimeObject3DRenderer.js");
}
if (includeCaptureManager) {
InsertUnique(includesFiles, "capturemanager.js");
}
}

void ExporterHelper::RemoveIncludes(bool pixiRenderers,
Expand Down
37 changes: 28 additions & 9 deletions GDJS/GDJS/IDE/ExporterHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <unordered_map>
#include <vector>

#include "GDCore/IDE/CaptureOptions.h"
#include "GDCore/String.h"
namespace gd {
class Project;
Expand All @@ -20,6 +21,8 @@ class SerializerElement;
class AbstractFileSystem;
class ResourcesManager;
class WholeProjectDiagnosticReport;
class CaptureOptions;
class Screenshot;
} // namespace gd
class wxProgressDialog;

Expand Down Expand Up @@ -48,7 +51,7 @@ struct PreviewExportOptions {
playerId(""),
playerUsername(""),
playerToken(""),
allowAuthenticationUsingIframeForPreview(false){};
allowAuthenticationUsingIframeForPreview(false) {};

/**
* \brief Set the address of the debugger server that the game should reach
Expand Down Expand Up @@ -216,16 +219,15 @@ struct PreviewExportOptions {
* \brief Set the level of crash reports to be sent to GDevelop APIs.
*/
PreviewExportOptions &SetCrashReportUploadLevel(
const gd::String& crashReportUploadLevel_) {
const gd::String &crashReportUploadLevel_) {
crashReportUploadLevel = crashReportUploadLevel_;
return *this;
}

/**
* \brief Set the context of the preview.
*/
PreviewExportOptions &SetPreviewContext(
const gd::String& previewContext_) {
PreviewExportOptions &SetPreviewContext(const gd::String &previewContext_) {
previewContext = previewContext_;
return *this;
}
Expand All @@ -234,7 +236,7 @@ struct PreviewExportOptions {
* \brief Set the GDevelop version so the game is aware of it.
*/
PreviewExportOptions &SetGDevelopVersionWithHash(
const gd::String& gdevelopVersionWithHash_) {
const gd::String &gdevelopVersionWithHash_) {
gdevelopVersionWithHash = gdevelopVersionWithHash_;
return *this;
}
Expand All @@ -243,19 +245,34 @@ struct PreviewExportOptions {
* \brief Set the template slug that was used to create the project.
*/
PreviewExportOptions &SetProjectTemplateSlug(
const gd::String& projectTemplateSlug_) {
const gd::String &projectTemplateSlug_) {
projectTemplateSlug = projectTemplateSlug_;
return *this;
}

/**
* \brief Set the source game id that was used to create the project.
*/
PreviewExportOptions &SetSourceGameId(const gd::String& sourceGameId_) {
PreviewExportOptions &SetSourceGameId(const gd::String &sourceGameId_) {
sourceGameId = sourceGameId_;
return *this;
}

/**
* \brief Set the capture options to be used for taking screenshots or videos
* of the preview.
*/
PreviewExportOptions &AddScreenshotCapture(int delayTimeInSeconds,
const gd::String &signedUrl,
const gd::String &publicUrl) {
gd::Screenshot screenshot;
screenshot.SetDelayTimeInSeconds(delayTimeInSeconds);
screenshot.SetSignedUrl(signedUrl);
screenshot.SetPublicUrl(publicUrl);
captureOptions.AddScreenshot(screenshot);
return *this;
}

gd::Project &project;
gd::String exportPath;
gd::String websocketDebuggerServerAddress;
Expand Down Expand Up @@ -283,6 +300,7 @@ struct PreviewExportOptions {
gd::String gdevelopVersionWithHash;
gd::String projectTemplateSlug;
gd::String sourceGameId;
gd::CaptureOptions captureOptions;
};

/**
Expand All @@ -298,7 +316,7 @@ struct ExportOptions {
exportPath(exportPath_),
target(""),
fallbackAuthorId(""),
fallbackAuthorUsername(""){};
fallbackAuthorUsername("") {};

/**
* \brief Set the fallback author info (if info not present in project
Expand Down Expand Up @@ -338,7 +356,7 @@ class ExporterHelper {
ExporterHelper(gd::AbstractFileSystem &fileSystem,
gd::String gdjsRoot_,
gd::String codeOutputDir);
virtual ~ExporterHelper(){};
virtual ~ExporterHelper() {};

/**
* \brief Return the error that occurred during the last export.
Expand Down Expand Up @@ -386,6 +404,7 @@ class ExporterHelper {
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
bool includeCaptureManager,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles);

Expand Down
83 changes: 83 additions & 0 deletions GDJS/Runtime/capturemanager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* GDevelop JS Platform
* Copyright 2013-2016 Florian Rival ([email protected]). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
export type CaptureOptions = {
screenshots?: {
delayTimeInSeconds: number;
signedUrl: string;
publicUrl: string;
}[];
ClementPasteau marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Helper function to convert a base64 string to a Blob, which can be uploaded to a server.
*/
const base64ToBlob = (base64) => {
const byteString = atob(base64.split(',')[1]);
const mimeString = base64.split(',')[0].split(':')[1].split(';')[0];

const arrayBuffer = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
arrayBuffer[i] = byteString.charCodeAt(i);
}

return new Blob([arrayBuffer], { type: mimeString });
};

/**
* Manage the captures (screenshots, videos, etc...) that need to be taken during the game.
*/
export class CaptureManager {
_gameRenderer: gdjs.RuntimeGameRenderer;
_captureOptions: CaptureOptions;

constructor(
renderer: gdjs.RuntimeGameRenderer,
captureOptions: CaptureOptions
) {
this._gameRenderer = renderer;
this._captureOptions = captureOptions;
}

/**
* To be called when the scene has started rendering.
*/
setupCaptureOptions(isPreview: boolean): void {
if (!isPreview || !this._captureOptions.screenshots) {
return;
}

for (const screenshotCaptureOption of this._captureOptions.screenshots) {
setTimeout(async () => {
await this.takeAndUploadScreenshot(screenshotCaptureOption.signedUrl);
}, screenshotCaptureOption.delayTimeInSeconds);
}
}

/**
* Take a screenshot and upload it to the server.
*/
async takeAndUploadScreenshot(signedUrl: string) {
const canvas = this._gameRenderer.getCanvas();
if (canvas) {
try {
const base64Data = canvas.toDataURL('image/png');
const blobData = base64ToBlob(base64Data);

await fetch(signedUrl, {
method: 'PUT',
body: blobData,
headers: {
'Content-Type': 'image/png',
},
});
} catch (error) {
console.error('Error while uploading screenshot:', error);
}
}
}
}
}
Loading
Loading