mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
397 Commits
a05b0cd0ef
...
shape-pain
Author | SHA1 | Date | |
---|---|---|---|
![]() |
62ed7a4998 | ||
![]() |
dc5dab7086 | ||
![]() |
dad7546099 | ||
![]() |
0a55eb46d3 | ||
![]() |
2678d86783 | ||
![]() |
b31618568a | ||
![]() |
b3b4681c51 | ||
![]() |
b1b7643efc | ||
![]() |
d43479cbe5 | ||
![]() |
76d43e1695 | ||
![]() |
c3ee9d9891 | ||
![]() |
313f7857e2 | ||
![]() |
3ac2aec229 | ||
![]() |
92e8dfdf04 | ||
![]() |
80726da56d | ||
![]() |
3098fb034f | ||
![]() |
9d0f172663 | ||
![]() |
e0d2f16dc1 | ||
![]() |
0578c45b0f | ||
![]() |
a8866a7bd6 | ||
![]() |
ec62e33530 | ||
![]() |
e1f12837f1 | ||
![]() |
732f3c14b9 | ||
![]() |
7fdad45192 | ||
![]() |
94590d3713 | ||
![]() |
de310512a3 | ||
![]() |
a9edd2f21a | ||
![]() |
0cc03bd4b5 | ||
![]() |
cdbcaf59d9 | ||
![]() |
4e9b09e426 | ||
![]() |
3d843a0170 | ||
![]() |
ee2f7fb8dc | ||
![]() |
a780601230 | ||
![]() |
cc42923e16 | ||
![]() |
0e8a223b24 | ||
![]() |
c2f17f9348 | ||
![]() |
0ba59d5d7f | ||
![]() |
6878a4cd75 | ||
![]() |
33eed58c62 | ||
![]() |
f516eff739 | ||
![]() |
baefc272f6 | ||
![]() |
2badc72dfb | ||
![]() |
5e54f02061 | ||
![]() |
827bb2d08d | ||
![]() |
0fda0baa48 | ||
![]() |
e655cc0661 | ||
![]() |
225d1b37ab | ||
![]() |
5a0ab9dffa | ||
![]() |
9300604d56 | ||
![]() |
02fcf1dbc2 | ||
![]() |
a70b2e2c2c | ||
![]() |
3e04b5a82f | ||
![]() |
cbcf0b7b70 | ||
![]() |
5d1fe83655 | ||
![]() |
7cf70a6fd7 | ||
![]() |
24c2dbc340 | ||
![]() |
290901849c | ||
![]() |
59721cba7e | ||
![]() |
53cd78bb45 | ||
![]() |
49f8ce9385 | ||
![]() |
de2a9725f9 | ||
![]() |
b2ec3b5387 | ||
![]() |
1873b5e592 | ||
![]() |
e47e35c090 | ||
![]() |
ae572683f1 | ||
![]() |
f8e387230f | ||
![]() |
9451e5969f | ||
![]() |
ad7ddf09a3 | ||
![]() |
8e70930d8d | ||
![]() |
ab3bda24dc | ||
![]() |
d33a7331b3 | ||
![]() |
4d3793815f | ||
![]() |
e1835d1144 | ||
![]() |
72068460e1 | ||
![]() |
5da76ae655 | ||
![]() |
b5b25ad710 | ||
![]() |
2279f069af | ||
![]() |
f0a68db0d4 | ||
![]() |
15ed28ad8d | ||
![]() |
4291d5597a | ||
![]() |
7af3fa5f2f | ||
![]() |
5ce9591f68 | ||
![]() |
d5929010a7 | ||
![]() |
30f2f5256b | ||
![]() |
806d59fb88 | ||
![]() |
e5f18ae2d8 | ||
![]() |
15a7fd1f85 | ||
![]() |
3678a0dd45 | ||
![]() |
197bd913b8 | ||
![]() |
baef911d61 | ||
![]() |
771e16e779 | ||
![]() |
a2cf8e694b | ||
![]() |
cb151ea30e | ||
![]() |
7d1ebc8963 | ||
![]() |
510a699c03 | ||
![]() |
08892b68d4 | ||
![]() |
e079fa4108 | ||
![]() |
5a26e883b8 | ||
![]() |
6bf5b389b5 | ||
![]() |
2837a2906a | ||
![]() |
8c63fae2f2 | ||
![]() |
f837c22290 | ||
![]() |
465bfa4deb | ||
![]() |
a047ecdf9c | ||
![]() |
04c28de00b | ||
![]() |
a7df6de044 | ||
![]() |
81c3199d00 | ||
![]() |
dbe93a4bfd | ||
![]() |
e0edbe8d57 | ||
![]() |
c25d51805f | ||
![]() |
ab695370bb | ||
![]() |
15ee216e33 | ||
![]() |
3fac0522c9 | ||
![]() |
8d502d7c5c | ||
![]() |
12d18c45bc | ||
![]() |
d255ab458b | ||
![]() |
fbfa0315de | ||
![]() |
9575705d29 | ||
![]() |
1223eaa348 | ||
![]() |
21ea077768 | ||
![]() |
11895decd9 | ||
![]() |
c0e1e9fac6 | ||
![]() |
bd9c631e1b | ||
![]() |
4a6e8ef664 | ||
![]() |
e25345000d | ||
![]() |
111d37fc15 | ||
![]() |
4a83c9eb59 | ||
![]() |
177cb2c519 | ||
![]() |
b5d69dee4c | ||
![]() |
fbdaebe575 | ||
![]() |
2a2fd75ca3 | ||
![]() |
af7563b4b7 | ||
![]() |
1861c3be41 | ||
![]() |
895dc625eb | ||
![]() |
52aea76677 | ||
![]() |
4f87191176 | ||
![]() |
43d4e2e8cc | ||
![]() |
cbfaa13978 | ||
![]() |
0aba5cf551 | ||
![]() |
a3ef07c163 | ||
![]() |
a8ede5eee7 | ||
![]() |
ce965ca31c | ||
![]() |
039fbb8b1b | ||
![]() |
80c1e67146 | ||
![]() |
2591cabdd0 | ||
![]() |
45620d6bf4 | ||
![]() |
89a59cdd35 | ||
![]() |
17b819a423 | ||
![]() |
afb4e3c1c6 | ||
![]() |
e5f8fe1bf8 | ||
![]() |
30de5c6fa9 | ||
![]() |
79a29cf7d5 | ||
![]() |
98a1323bf5 | ||
![]() |
0fe6585897 | ||
![]() |
33dd605c57 | ||
![]() |
bab0c227f8 | ||
![]() |
5ad2be4a8a | ||
![]() |
39e18678f5 | ||
![]() |
f54b13a9f5 | ||
![]() |
703adc090a | ||
![]() |
81f0047dab | ||
![]() |
ef70add27b | ||
![]() |
d45932cc04 | ||
![]() |
081a97a5fc | ||
![]() |
2b3cedb441 | ||
![]() |
76426117ff | ||
![]() |
e8720780eb | ||
![]() |
e5ed642121 | ||
![]() |
a6602292b7 | ||
![]() |
859d8e08a0 | ||
![]() |
8128adfd7b | ||
![]() |
f9317dd17f | ||
![]() |
32a169014a | ||
![]() |
37d3fd99eb | ||
![]() |
e0973a8231 | ||
![]() |
876ce0d0a5 | ||
![]() |
9f614ce7e0 | ||
![]() |
63de997e60 | ||
![]() |
d2aa49fd1c | ||
![]() |
d355c16bdf | ||
![]() |
8c7f5d1ea8 | ||
![]() |
08635de08e | ||
![]() |
4dc8676848 | ||
![]() |
164a230cbc | ||
![]() |
f3dc551284 | ||
![]() |
ec674e85d6 | ||
![]() |
32f47900cd | ||
![]() |
320774c8d8 | ||
![]() |
1594f44a72 | ||
![]() |
726d3a8816 | ||
![]() |
097f13db42 | ||
![]() |
15585c2007 | ||
![]() |
ca0a2c3215 | ||
![]() |
d2820bdf2a | ||
![]() |
947e5eb9a3 | ||
![]() |
5901e34f6d | ||
![]() |
1c942d2f9d | ||
![]() |
b419bf8f35 | ||
![]() |
7edfa1284a | ||
![]() |
32e4006b2c | ||
![]() |
e773a1dbfc | ||
![]() |
3942214ba1 | ||
![]() |
461fc36401 | ||
![]() |
fa9198174e | ||
![]() |
96460d92d3 | ||
![]() |
e0d376c15b | ||
![]() |
46e0301dd0 | ||
![]() |
14aa26c651 | ||
![]() |
ab59dfaa86 | ||
![]() |
4841f46d95 | ||
![]() |
faccc6a6f2 | ||
![]() |
f336e76a86 | ||
![]() |
811604d15a | ||
![]() |
dd005941e9 | ||
![]() |
311381332f | ||
![]() |
50173c4127 | ||
![]() |
e030adbb72 | ||
![]() |
3762ccf639 | ||
![]() |
d2758400e0 | ||
![]() |
1551ec440e | ||
![]() |
bd493e39e4 | ||
![]() |
2fb663b63f | ||
![]() |
3878f35107 | ||
![]() |
e055c0edbf | ||
![]() |
a02afc4c68 | ||
![]() |
30130df6fb | ||
![]() |
400cd2e22c | ||
![]() |
dc25a0e87f | ||
![]() |
d21abd97a7 | ||
![]() |
bd38eaf924 | ||
![]() |
360e98b3ee | ||
![]() |
e03908da83 | ||
![]() |
64e7afea7c | ||
![]() |
13077f7ce8 | ||
![]() |
ef68f4e7f6 | ||
![]() |
eaeef23bc6 | ||
![]() |
5a3b12a257 | ||
![]() |
794b74a90c | ||
![]() |
ac4a500fff | ||
![]() |
13a5939550 | ||
![]() |
e560f0f08b | ||
![]() |
3894720a93 | ||
![]() |
2a89b95510 | ||
![]() |
e8e4bf07df | ||
![]() |
4d3b98f78b | ||
![]() |
42ee73b046 | ||
![]() |
0a5df21c1b | ||
![]() |
b0ea0c262b | ||
![]() |
ffa9c45692 | ||
![]() |
15f6f16d22 | ||
![]() |
c3240b6767 | ||
![]() |
2f7fe905b8 | ||
![]() |
0de1d42d73 | ||
![]() |
c4c8931c73 | ||
![]() |
1c9eb654ce | ||
![]() |
ee5b55cfbb | ||
![]() |
7e121526ca | ||
![]() |
79dfec1feb | ||
![]() |
d30a278b97 | ||
![]() |
5ae9e80987 | ||
![]() |
245fb1b3ab | ||
![]() |
bedf8a48a2 | ||
![]() |
3c77c9e2d8 | ||
![]() |
92ba2c3111 | ||
![]() |
3ea754bba2 | ||
![]() |
a3429fb687 | ||
![]() |
476a107c46 | ||
![]() |
f98d28bee2 | ||
![]() |
df5ee8732a | ||
![]() |
bd500f5a90 | ||
![]() |
536518f4bb | ||
![]() |
7e11936fee | ||
![]() |
b0da0508e1 | ||
![]() |
48cc5a27f6 | ||
![]() |
d470513f84 | ||
![]() |
baaeb4317f | ||
![]() |
40156c5e74 | ||
![]() |
da0a47b487 | ||
![]() |
ee12b0b372 | ||
![]() |
31f7ec3a17 | ||
![]() |
3ec7617b14 | ||
![]() |
69e9eebd8e | ||
![]() |
268af00ce8 | ||
![]() |
ad748e4ed5 | ||
![]() |
f933736f1f | ||
![]() |
89861de544 | ||
![]() |
d503fff76b | ||
![]() |
e8dc9e3b20 | ||
![]() |
e40cdfca95 | ||
![]() |
e357fca9c0 | ||
![]() |
6b4f022157 | ||
![]() |
29c276d061 | ||
![]() |
b28eec1071 | ||
![]() |
c3c774579d | ||
![]() |
193977d6d2 | ||
![]() |
931bb10edb | ||
![]() |
408b9c151c | ||
![]() |
bd96a63087 | ||
![]() |
dd5eb3b96f | ||
![]() |
cd46428b81 | ||
![]() |
29926f057e | ||
![]() |
5444896b76 | ||
![]() |
4adb7295bd | ||
![]() |
2edd127d68 | ||
![]() |
eb45ed8b9a | ||
![]() |
3a81c76741 | ||
![]() |
14e2a6236a | ||
![]() |
1ea065dec6 | ||
![]() |
db42bbef62 | ||
![]() |
bd0b610a90 | ||
![]() |
f46213ce6e | ||
![]() |
0909e1d5fe | ||
![]() |
95c15b2edb | ||
![]() |
e48115336d | ||
![]() |
daa957fa7b | ||
![]() |
3d5a997633 | ||
![]() |
ca15d0316d | ||
![]() |
96a9c28e44 | ||
![]() |
006f8b1900 | ||
![]() |
d576b3ede8 | ||
![]() |
195664ee09 | ||
![]() |
bf85daf354 | ||
![]() |
d3192734a1 | ||
![]() |
16f5ecdd0b | ||
![]() |
b17fa8e9ae | ||
![]() |
88bcd797e9 | ||
![]() |
577d605b87 | ||
![]() |
a5e29a7f14 | ||
![]() |
7f27526c7c | ||
![]() |
f3adb4e876 | ||
![]() |
c571a65a32 | ||
![]() |
55f38231a9 | ||
![]() |
6a2e37d147 | ||
![]() |
f6ec9634ce | ||
![]() |
eedb030619 | ||
![]() |
c828c2cace | ||
![]() |
b4188893a0 | ||
![]() |
8ea26ec148 | ||
![]() |
bf73e430f8 | ||
![]() |
343f6a572d | ||
![]() |
66134e95ce | ||
![]() |
1ffb03ccd5 | ||
![]() |
998bf76e72 | ||
![]() |
47553bc020 | ||
![]() |
c84b2410b9 | ||
![]() |
fbbac73e94 | ||
![]() |
c2f0f9b9a3 | ||
![]() |
4d02ae092b | ||
![]() |
7db2849404 | ||
![]() |
d5a3b55e98 | ||
![]() |
79206fbfee | ||
![]() |
2089e29664 | ||
![]() |
38c5425d0c | ||
![]() |
b0d45e80de | ||
![]() |
2ac5beee0c | ||
![]() |
c99c73fa96 | ||
![]() |
f796228d06 | ||
![]() |
ca3836b3c7 | ||
![]() |
918f8ec989 | ||
![]() |
f44d9682ee | ||
![]() |
ce6800164d | ||
![]() |
4afb2de178 | ||
![]() |
9865fd3663 | ||
![]() |
5501527974 | ||
![]() |
3e4826bafc | ||
![]() |
d914f9f5be | ||
![]() |
ab8c90dd41 | ||
![]() |
8882841a8d | ||
![]() |
3d357950f9 | ||
![]() |
ef5d2651c0 | ||
![]() |
1dd4bb9b7a | ||
![]() |
682e6f6b03 | ||
![]() |
b468c28ae8 | ||
![]() |
9fc9452a08 | ||
![]() |
3b176b7152 | ||
![]() |
f1d1a9b66b | ||
![]() |
c0d2f491a8 | ||
![]() |
aeecce2ea8 | ||
![]() |
647ee149ff | ||
![]() |
22313e148a | ||
![]() |
31f2d7ce2e | ||
![]() |
4a0efec6c2 | ||
![]() |
5f555df5c1 | ||
![]() |
53a611a1b9 | ||
![]() |
14511b23af | ||
![]() |
fa2371274d | ||
![]() |
0aea8dfa0f | ||
![]() |
81ca18098d | ||
![]() |
b6e44a022f | ||
![]() |
1a8eee2477 | ||
![]() |
d0ef92da03 | ||
![]() |
9c98cb3b3b | ||
![]() |
3681542056 | ||
![]() |
7c0bf135d7 | ||
![]() |
9a31dd046c | ||
![]() |
74401a1f9c | ||
![]() |
cedc6ea3e9 |
@@ -813,6 +813,13 @@ gd::String PlatformExtension::GetObjectFullType(const gd::String& extensionName,
|
||||
return extensionName + separator + objectName;
|
||||
}
|
||||
|
||||
gd::String PlatformExtension::GetVariantFullType(const gd::String& extensionName,
|
||||
const gd::String& objectName,
|
||||
const gd::String& variantName) {
|
||||
const auto& separator = GetNamespaceSeparator();
|
||||
return extensionName + separator + objectName + separator + variantName;
|
||||
}
|
||||
|
||||
gd::String PlatformExtension::GetExtensionFromFullObjectType(
|
||||
const gd::String& type) {
|
||||
const auto separatorIndex =
|
||||
|
@@ -663,6 +663,10 @@ class GD_CORE_API PlatformExtension {
|
||||
static gd::String GetObjectFullType(const gd::String& extensionName,
|
||||
const gd::String& objectName);
|
||||
|
||||
static gd::String GetVariantFullType(const gd::String& extensionName,
|
||||
const gd::String& objectName,
|
||||
const gd::String& variantName);
|
||||
|
||||
static gd::String GetExtensionFromFullObjectType(const gd::String& type);
|
||||
|
||||
static gd::String GetObjectNameFromFullObjectType(const gd::String& type);
|
||||
|
23
Core/GDCore/IDE/Events/UsedObjectTypeFinder.cpp
Normal file
23
Core/GDCore/IDE/Events/UsedObjectTypeFinder.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "UsedObjectTypeFinder.h"
|
||||
|
||||
#include "GDCore/Events/Instruction.h"
|
||||
#include "GDCore/IDE/ProjectBrowserHelper.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
bool UsedObjectTypeFinder::ScanProject(gd::Project &project,
|
||||
const gd::String &objectType) {
|
||||
UsedObjectTypeFinder worker(project, objectType);
|
||||
gd::ProjectBrowserHelper::ExposeProjectObjects(project, worker);
|
||||
return worker.hasFoundObjectType;
|
||||
};
|
||||
|
||||
void UsedObjectTypeFinder::DoVisitObject(gd::Object &object) {
|
||||
if (!hasFoundObjectType && object.GetType() == objectType) {
|
||||
hasFoundObjectType = true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gd
|
39
Core/GDCore/IDE/Events/UsedObjectTypeFinder.h
Normal file
39
Core/GDCore/IDE/Events/UsedObjectTypeFinder.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 <set>
|
||||
|
||||
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
|
||||
#include "GDCore/Extensions/Metadata/SourceFileMetadata.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryObjectsWorker.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
class Project;
|
||||
class Object;
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
|
||||
class GD_CORE_API UsedObjectTypeFinder : public ArbitraryObjectsWorker {
|
||||
public:
|
||||
static bool ScanProject(gd::Project &project, const gd::String &objectType);
|
||||
|
||||
private:
|
||||
UsedObjectTypeFinder(gd::Project &project_, const gd::String &objectType_)
|
||||
: project(project_), objectType(objectType_){};
|
||||
gd::Project &project;
|
||||
const gd::String &objectType;
|
||||
bool hasFoundObjectType = false;
|
||||
|
||||
// Object Visitor
|
||||
void DoVisitObject(gd::Object &object) override;
|
||||
};
|
||||
|
||||
}; // namespace gd
|
@@ -7,7 +7,6 @@
|
||||
#include <map>
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/IDE/AbstractFileSystem.h"
|
||||
#include "GDCore/IDE/Project/ResourcesAbsolutePathChecker.h"
|
||||
#include "GDCore/IDE/Project/ResourcesMergingHelper.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
@@ -26,42 +25,37 @@ bool ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
bool preserveAbsoluteFilenames,
|
||||
bool preserveDirectoryStructure) {
|
||||
if (updateOriginalProject) {
|
||||
gd::ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
originalProject, originalProject, fs, destinationDirectory,
|
||||
preserveAbsoluteFilenames, preserveDirectoryStructure);
|
||||
gd::ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
|
||||
originalProject, fs, destinationDirectory, preserveAbsoluteFilenames,
|
||||
preserveDirectoryStructure);
|
||||
} else {
|
||||
gd::Project clonedProject = originalProject;
|
||||
gd::ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
originalProject, clonedProject, fs, destinationDirectory,
|
||||
preserveAbsoluteFilenames, preserveDirectoryStructure);
|
||||
gd::ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
|
||||
clonedProject, fs, destinationDirectory, preserveAbsoluteFilenames,
|
||||
preserveDirectoryStructure);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
gd::Project& originalProject,
|
||||
gd::Project& clonedProject,
|
||||
bool ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
|
||||
gd::Project& project,
|
||||
AbstractFileSystem& fs,
|
||||
gd::String destinationDirectory,
|
||||
bool preserveAbsoluteFilenames,
|
||||
bool preserveDirectoryStructure) {
|
||||
|
||||
// Check if there are some resources with absolute filenames
|
||||
gd::ResourcesAbsolutePathChecker absolutePathChecker(originalProject.GetResourcesManager(), fs);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(originalProject, absolutePathChecker);
|
||||
|
||||
auto projectDirectory = fs.DirNameFrom(originalProject.GetProjectFile());
|
||||
auto projectDirectory = fs.DirNameFrom(project.GetProjectFile());
|
||||
std::cout << "Copying all resources from " << projectDirectory << " to "
|
||||
<< destinationDirectory << "..." << std::endl;
|
||||
|
||||
// Get the resources to be copied
|
||||
gd::ResourcesMergingHelper resourcesMergingHelper(
|
||||
clonedProject.GetResourcesManager(), fs);
|
||||
project.GetResourcesManager(), fs);
|
||||
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
|
||||
resourcesMergingHelper.PreserveDirectoriesStructure(
|
||||
preserveDirectoryStructure);
|
||||
resourcesMergingHelper.PreserveAbsoluteFilenames(preserveAbsoluteFilenames);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(clonedProject,
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project,
|
||||
resourcesMergingHelper);
|
||||
|
||||
// Copy resources
|
||||
|
@@ -50,12 +50,10 @@ class GD_CORE_API ProjectResourcesCopier {
|
||||
bool preserveDirectoryStructure = true);
|
||||
|
||||
private:
|
||||
static bool CopyAllResourcesTo(gd::Project& originalProject,
|
||||
gd::Project& clonedProject,
|
||||
gd::AbstractFileSystem& fs,
|
||||
gd::String destinationDirectory,
|
||||
bool preserveAbsoluteFilenames = true,
|
||||
bool preserveDirectoryStructure = true);
|
||||
static bool AdaptFilePathsAndCopyAllResourcesTo(
|
||||
gd::Project &project, gd::AbstractFileSystem &fs,
|
||||
gd::String destinationDirectory, bool preserveAbsoluteFilenames = true,
|
||||
bool preserveDirectoryStructure = true);
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include "ResourcesAbsolutePathChecker.h"
|
||||
#include "GDCore/IDE/AbstractFileSystem.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
void ResourcesAbsolutePathChecker::ExposeFile(gd::String& resourceFilename) {
|
||||
if (fs.IsAbsolute(resourceFilename)) hasAbsoluteFilenames = true;
|
||||
}
|
||||
|
||||
} // namespace gd
|
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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/AbstractFileSystem.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* \brief Helper used to check if a project has at least a resource with an
|
||||
* absolute filename.
|
||||
*
|
||||
* \see ArbitraryResourceWorker
|
||||
*
|
||||
* \ingroup IDE
|
||||
*/
|
||||
class GD_CORE_API ResourcesAbsolutePathChecker
|
||||
: public ArbitraryResourceWorker {
|
||||
public:
|
||||
ResourcesAbsolutePathChecker(gd::ResourcesManager &resourcesManager,
|
||||
AbstractFileSystem &fileSystem)
|
||||
: ArbitraryResourceWorker(resourcesManager), hasAbsoluteFilenames(false),
|
||||
fs(fileSystem){};
|
||||
virtual ~ResourcesAbsolutePathChecker(){};
|
||||
|
||||
/**
|
||||
* Return true if there is at least a resource with an absolute filename.
|
||||
*/
|
||||
bool HasResourceWithAbsoluteFilenames() const {
|
||||
return hasAbsoluteFilenames;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is a resource with an absolute path
|
||||
*/
|
||||
virtual void ExposeFile(gd::String& resource);
|
||||
|
||||
private:
|
||||
bool hasAbsoluteFilenames;
|
||||
AbstractFileSystem& fs;
|
||||
};
|
||||
|
||||
} // namespace gd
|
@@ -22,6 +22,14 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) {
|
||||
resourceFullFilename = gd::AbstractFileSystem::NormalizeSeparator(
|
||||
resourceFullFilename); // Protect against \ on Linux.
|
||||
|
||||
if (shouldUseOriginalAbsoluteFilenames) {
|
||||
// There is no need to fill `newFilenames` and `oldFilenames` since the file
|
||||
// location stays the same.
|
||||
fs.MakeAbsolute(resourceFullFilename, baseDirectory);
|
||||
resourceFilename = resourceFullFilename;
|
||||
return;
|
||||
}
|
||||
|
||||
// In the case of absolute filenames that we don't want to preserve, or
|
||||
// in the case of copying files without preserving relative folders, the new
|
||||
// names will be generated from the filename alone (with collision protection).
|
||||
|
@@ -3,8 +3,7 @@
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#ifndef RESOURCESMERGINGHELPER_H
|
||||
#define RESOURCESMERGINGHELPER_H
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@@ -58,6 +57,15 @@ public:
|
||||
preserveAbsoluteFilenames = preserveAbsoluteFilenames_;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Set if the absolute filenames of original files must be used for
|
||||
* any resource.
|
||||
*/
|
||||
void SetShouldUseOriginalAbsoluteFilenames(
|
||||
bool shouldUseOriginalAbsoluteFilenames_ = true) {
|
||||
shouldUseOriginalAbsoluteFilenames = shouldUseOriginalAbsoluteFilenames_;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Return a map containing the resources old absolute filename as key,
|
||||
* and the resources new filenames as value. The new filenames are relative to
|
||||
@@ -93,10 +101,13 @@ public:
|
||||
///< absolute (C:\MyFile.png will not be
|
||||
///< transformed into a relative filename
|
||||
///< (MyFile.png).
|
||||
/**
|
||||
* Set to true if the absolute filenames of original files must be used for
|
||||
* any resource.
|
||||
*/
|
||||
bool shouldUseOriginalAbsoluteFilenames = false;
|
||||
gd::AbstractFileSystem&
|
||||
fs; ///< The gd::AbstractFileSystem used to manipulate files.
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif // RESOURCESMERGINGHELPER_H
|
||||
|
@@ -6,6 +6,7 @@
|
||||
#include "SceneResourcesFinder.h"
|
||||
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
#include "GDCore/Project/EventsBasedObjectVariant.h"
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
@@ -27,6 +28,14 @@ std::set<gd::String> SceneResourcesFinder::FindSceneResources(gd::Project &proje
|
||||
return resourceWorker.resourceNames;
|
||||
}
|
||||
|
||||
std::set<gd::String> SceneResourcesFinder::FindEventsBasedObjectVariantResources(gd::Project &project,
|
||||
gd::EventsBasedObjectVariant &variant) {
|
||||
gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager());
|
||||
|
||||
gd::ResourceExposer::ExposeEventsBasedObjectVariantResources(project, variant, resourceWorker);
|
||||
return resourceWorker.resourceNames;
|
||||
}
|
||||
|
||||
void SceneResourcesFinder::AddUsedResource(gd::String &resourceName) {
|
||||
if (resourceName.empty()) {
|
||||
return;
|
||||
|
@@ -15,6 +15,7 @@ namespace gd {
|
||||
class Project;
|
||||
class Layout;
|
||||
class SerializerElement;
|
||||
class EventsBasedObjectVariant;
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
@@ -27,7 +28,7 @@ namespace gd {
|
||||
class SceneResourcesFinder : private gd::ArbitraryResourceWorker {
|
||||
public:
|
||||
/**
|
||||
* @brief Find resource usages in a given scenes.
|
||||
* @brief Find resource usages in a given scene.
|
||||
*
|
||||
* It doesn't include resources used globally.
|
||||
*/
|
||||
@@ -41,6 +42,13 @@ public:
|
||||
*/
|
||||
static std::set<gd::String> FindProjectResources(gd::Project &project);
|
||||
|
||||
/**
|
||||
* @brief Find resource usages in a given events-based object variant.
|
||||
*/
|
||||
static std::set<gd::String>
|
||||
FindEventsBasedObjectVariantResources(gd::Project &project,
|
||||
gd::EventsBasedObjectVariant &variant);
|
||||
|
||||
virtual ~SceneResourcesFinder(){};
|
||||
|
||||
private:
|
||||
|
@@ -332,6 +332,12 @@ void ProjectBrowserHelper::ExposeLayoutObjects(gd::Layout &layout,
|
||||
worker.Launch(layout.GetObjects());
|
||||
}
|
||||
|
||||
void ProjectBrowserHelper::ExposeEventsBasedObjectVariantObjects(
|
||||
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
|
||||
gd::ArbitraryObjectsWorker &worker) {
|
||||
worker.Launch(eventsBasedObjectVariant.GetObjects());
|
||||
}
|
||||
|
||||
void ProjectBrowserHelper::ExposeProjectFunctions(
|
||||
gd::Project &project, gd::ArbitraryEventsFunctionsWorker &worker) {
|
||||
|
||||
|
@@ -13,6 +13,7 @@ class EventsFunctionsExtension;
|
||||
class EventsFunction;
|
||||
class EventsBasedBehavior;
|
||||
class EventsBasedObject;
|
||||
class EventsBasedObjectVariant;
|
||||
class ArbitraryEventsWorker;
|
||||
class ArbitraryEventsWorkerWithContext;
|
||||
class ArbitraryEventsFunctionsWorker;
|
||||
@@ -207,6 +208,17 @@ public:
|
||||
static void ExposeLayoutObjects(gd::Layout &layout,
|
||||
gd::ArbitraryObjectsWorker &worker);
|
||||
|
||||
/**
|
||||
* \brief Call the specified worker on all ObjectContainers of the
|
||||
* events-based object variant.
|
||||
*
|
||||
* This should be the preferred way to traverse all the objects of an
|
||||
* events-based object variant.
|
||||
*/
|
||||
static void ExposeEventsBasedObjectVariantObjects(
|
||||
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
|
||||
gd::ArbitraryObjectsWorker &worker);
|
||||
|
||||
/**
|
||||
* \brief Call the specified worker on all FunctionsContainers of the project
|
||||
* (global, layouts...)
|
||||
|
@@ -116,6 +116,34 @@ void ResourceExposer::ExposeLayoutResources(
|
||||
project, layout, eventWorker);
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeEventsBasedObjectVariantResources(
|
||||
gd::Project &project,
|
||||
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
|
||||
gd::ArbitraryResourceWorker &worker) {
|
||||
// Expose object configuration resources
|
||||
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
|
||||
gd::ProjectBrowserHelper::ExposeEventsBasedObjectVariantObjects(
|
||||
eventsBasedObjectVariant, objectWorker);
|
||||
|
||||
// Expose layer effect resources
|
||||
auto &layers = eventsBasedObjectVariant.GetLayers();
|
||||
for (std::size_t layerIndex = 0; layerIndex < layers.GetLayersCount();
|
||||
layerIndex++) {
|
||||
auto &layer = layers.GetLayer(layerIndex);
|
||||
|
||||
auto &effects = layer.GetEffects();
|
||||
for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount();
|
||||
effectIndex++) {
|
||||
auto &effect = effects.GetEffect(effectIndex);
|
||||
gd::ResourceExposer::ExposeEffectResources(project.GetCurrentPlatform(),
|
||||
effect, worker);
|
||||
}
|
||||
}
|
||||
// We don't check the events because it would cost too much to do it for every
|
||||
// variant. Resource usage in events-based object events and their
|
||||
// dependencies should be rare.
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeEffectResources(
|
||||
gd::Platform &platform,
|
||||
gd::Effect &effect,
|
||||
|
@@ -9,10 +9,11 @@ namespace gd {
|
||||
class Platform;
|
||||
class Project;
|
||||
class ArbitraryResourceWorker;
|
||||
class EventsBasedObjectVariant;
|
||||
class EventsFunctionsExtension;
|
||||
class Effect;
|
||||
class Layout;
|
||||
} // namespace gd
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
|
||||
@@ -20,7 +21,7 @@ namespace gd {
|
||||
* \brief
|
||||
*/
|
||||
class GD_CORE_API ResourceExposer {
|
||||
public:
|
||||
public:
|
||||
/**
|
||||
* \brief Called ( e.g. during compilation ) so as to inventory internal
|
||||
* resources, sometimes update their filename or any other work or resources.
|
||||
@@ -50,6 +51,14 @@ class GD_CORE_API ResourceExposer {
|
||||
gd::Layout &layout,
|
||||
gd::ArbitraryResourceWorker &worker);
|
||||
|
||||
/**
|
||||
* @brief Expose the resources used in a given events-based object variant.
|
||||
*/
|
||||
static void ExposeEventsBasedObjectVariantResources(
|
||||
gd::Project &project,
|
||||
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
|
||||
gd::ArbitraryResourceWorker &worker);
|
||||
|
||||
/**
|
||||
* @brief Expose the resources used in a given effect.
|
||||
*/
|
||||
|
@@ -60,6 +60,18 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
|
||||
} else {
|
||||
SetHasCustomDepth(false);
|
||||
}
|
||||
if (element.HasChild("defaultWidth") ||
|
||||
element.HasAttribute("defaultWidth")) {
|
||||
defaultWidth = element.GetDoubleAttribute("defaultWidth");
|
||||
}
|
||||
if (element.HasChild("defaultHeight") ||
|
||||
element.HasAttribute("defaultHeight")) {
|
||||
defaultHeight = element.GetDoubleAttribute("defaultHeight");
|
||||
}
|
||||
if (element.HasChild("defaultDepth") ||
|
||||
element.HasAttribute("defaultDepth")) {
|
||||
defaultDepth = element.GetDoubleAttribute("defaultDepth");
|
||||
}
|
||||
SetZOrder(element.GetIntAttribute("zOrder", 0, "plan"));
|
||||
SetOpacity(element.GetIntAttribute("opacity", 255));
|
||||
SetLayer(element.GetStringAttribute("layer"));
|
||||
@@ -74,45 +86,51 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
|
||||
if (persistentUuid.empty()) ResetPersistentUuid();
|
||||
|
||||
numberProperties.clear();
|
||||
const SerializerElement& numberPropertiesElement =
|
||||
element.GetChild("numberProperties", 0, "floatInfos");
|
||||
numberPropertiesElement.ConsiderAsArrayOf("property", "Info");
|
||||
for (std::size_t j = 0; j < numberPropertiesElement.GetChildrenCount(); ++j) {
|
||||
gd::String name =
|
||||
numberPropertiesElement.GetChild(j).GetStringAttribute("name");
|
||||
double value =
|
||||
numberPropertiesElement.GetChild(j).GetDoubleAttribute("value");
|
||||
if (element.HasChild("numberProperties", "floatInfos")) {
|
||||
const SerializerElement& numberPropertiesElement =
|
||||
element.GetChild("numberProperties", 0, "floatInfos");
|
||||
numberPropertiesElement.ConsiderAsArrayOf("property", "Info");
|
||||
for (std::size_t j = 0; j < numberPropertiesElement.GetChildrenCount(); ++j) {
|
||||
gd::String name =
|
||||
numberPropertiesElement.GetChild(j).GetStringAttribute("name");
|
||||
double value =
|
||||
numberPropertiesElement.GetChild(j).GetDoubleAttribute("value");
|
||||
|
||||
// Compatibility with GD <= 5.1.164
|
||||
if (name == "z") {
|
||||
SetZ(value);
|
||||
} else if (name == "rotationX") {
|
||||
SetRotationX(value);
|
||||
} else if (name == "rotationY") {
|
||||
SetRotationY(value);
|
||||
} else if (name == "depth") {
|
||||
SetHasCustomDepth(true);
|
||||
SetCustomDepth(value);
|
||||
}
|
||||
// end of compatibility code
|
||||
else {
|
||||
numberProperties[name] = value;
|
||||
// Compatibility with GD <= 5.1.164
|
||||
if (name == "z") {
|
||||
SetZ(value);
|
||||
} else if (name == "rotationX") {
|
||||
SetRotationX(value);
|
||||
} else if (name == "rotationY") {
|
||||
SetRotationY(value);
|
||||
} else if (name == "depth") {
|
||||
SetHasCustomDepth(true);
|
||||
SetCustomDepth(value);
|
||||
}
|
||||
// end of compatibility code
|
||||
else {
|
||||
numberProperties[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stringProperties.clear();
|
||||
const SerializerElement& stringPropElement =
|
||||
element.GetChild("stringProperties", 0, "stringInfos");
|
||||
stringPropElement.ConsiderAsArrayOf("property", "Info");
|
||||
for (std::size_t j = 0; j < stringPropElement.GetChildrenCount(); ++j) {
|
||||
gd::String name = stringPropElement.GetChild(j).GetStringAttribute("name");
|
||||
gd::String value =
|
||||
stringPropElement.GetChild(j).GetStringAttribute("value");
|
||||
stringProperties[name] = value;
|
||||
if (element.HasChild("stringProperties", "stringInfos")) {
|
||||
const SerializerElement& stringPropElement =
|
||||
element.GetChild("stringProperties", 0, "stringInfos");
|
||||
stringPropElement.ConsiderAsArrayOf("property", "Info");
|
||||
for (std::size_t j = 0; j < stringPropElement.GetChildrenCount(); ++j) {
|
||||
gd::String name = stringPropElement.GetChild(j).GetStringAttribute("name");
|
||||
gd::String value =
|
||||
stringPropElement.GetChild(j).GetStringAttribute("value");
|
||||
stringProperties[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
GetVariables().UnserializeFrom(
|
||||
element.GetChild("initialVariables", 0, "InitialVariables"));
|
||||
if (element.HasChild("initialVariables", "InitialVariables")) {
|
||||
GetVariables().UnserializeFrom(
|
||||
element.GetChild("initialVariables", 0, "InitialVariables"));
|
||||
}
|
||||
}
|
||||
|
||||
void InitialInstance::SerializeTo(SerializerElement& element) const {
|
||||
@@ -133,6 +151,8 @@ void InitialInstance::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("width", GetCustomWidth());
|
||||
element.SetAttribute("height", GetCustomHeight());
|
||||
if (HasCustomDepth()) element.SetAttribute("depth", GetCustomDepth());
|
||||
// defaultWidth, defaultHeight and defaultDepth are not serialized
|
||||
// because they are evaluated by InGameEditor.
|
||||
if (IsLocked()) element.SetAttribute("locked", IsLocked());
|
||||
if (IsSealed()) element.SetAttribute("sealed", IsSealed());
|
||||
if (ShouldKeepRatio()) element.SetAttribute("keepRatio", ShouldKeepRatio());
|
||||
|
@@ -219,6 +219,13 @@ class GD_CORE_API InitialInstance {
|
||||
double GetCustomDepth() const { return depth; }
|
||||
void SetCustomDepth(double depth_) { depth = depth_; }
|
||||
|
||||
double GetDefaultWidth() const { return defaultWidth; }
|
||||
double GetDefaultHeight() const { return defaultHeight; }
|
||||
double GetDefaultDepth() const { return defaultDepth; }
|
||||
void SetDefaultWidth(double width_) { defaultWidth = width_; }
|
||||
void SetDefaultHeight(double height_) { defaultHeight = height_; }
|
||||
void SetDefaultDepth(double depth_) { defaultDepth = depth_; }
|
||||
|
||||
/**
|
||||
* \brief Return true if the instance is locked and cannot be moved in the
|
||||
* IDE.
|
||||
@@ -366,7 +373,11 @@ class GD_CORE_API InitialInstance {
|
||||
*/
|
||||
InitialInstance& ResetPersistentUuid();
|
||||
|
||||
const gd::String& GetPersistentUuid() const { return persistentUuid; }
|
||||
/**
|
||||
* \brief Reset the persistent UUID used to recognize
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
const gd::String& GetPersistentUuid() const { return persistentUuid; }
|
||||
///@}
|
||||
|
||||
private:
|
||||
@@ -395,6 +406,9 @@ class GD_CORE_API InitialInstance {
|
||||
double width; ///< Instance custom width
|
||||
double height; ///< Instance custom height
|
||||
double depth; ///< Instance custom depth
|
||||
double defaultWidth = 0; ///< Instance default width as reported by InGameEditor
|
||||
double defaultHeight = 0; ///< Instance default height as reported by InGameEditor
|
||||
double defaultDepth = 0; ///< Instance default depth as reported by InGameEditor
|
||||
gd::VariablesContainer initialVariables; ///< Instance specific variables
|
||||
bool locked; ///< True if the instance is locked
|
||||
bool sealed; ///< True if the instance is sealed
|
||||
|
@@ -23,6 +23,7 @@ Layer::Layer()
|
||||
camera3DNearPlaneDistance(3),
|
||||
camera3DFarPlaneDistance(10000),
|
||||
camera3DFieldOfView(45),
|
||||
camera2DPlaneMaxDrawingDistance(5000),
|
||||
ambientLightColorR(200),
|
||||
ambientLightColorG(200),
|
||||
ambientLightColorB(200) {}
|
||||
@@ -56,6 +57,8 @@ void Layer::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("camera3DFarPlaneDistance",
|
||||
GetCamera3DFarPlaneDistance());
|
||||
element.SetAttribute("camera3DFieldOfView", GetCamera3DFieldOfView());
|
||||
element.SetAttribute("camera2DPlaneMaxDrawingDistance",
|
||||
GetCamera2DPlaneMaxDrawingDistance());
|
||||
|
||||
SerializerElement& camerasElement = element.AddChild("cameras");
|
||||
camerasElement.ConsiderAsArrayOf("camera");
|
||||
@@ -99,6 +102,8 @@ void Layer::UnserializeFrom(const SerializerElement& element) {
|
||||
"camera3DFarPlaneDistance", 10000, "threeDFarPlaneDistance"));
|
||||
SetCamera3DFieldOfView(element.GetDoubleAttribute(
|
||||
"camera3DFieldOfView", 45, "threeDFieldOfView"));
|
||||
SetCamera2DPlaneMaxDrawingDistance(element.GetDoubleAttribute(
|
||||
"camera2DPlaneMaxDrawingDistance", 5000));
|
||||
|
||||
cameras.clear();
|
||||
SerializerElement& camerasElement = element.GetChild("cameras");
|
||||
|
@@ -182,6 +182,8 @@ class GD_CORE_API Layer {
|
||||
}
|
||||
double GetCamera3DFieldOfView() const { return camera3DFieldOfView; }
|
||||
void SetCamera3DFieldOfView(double angle) { camera3DFieldOfView = angle; }
|
||||
double GetCamera2DPlaneMaxDrawingDistance() const { return camera2DPlaneMaxDrawingDistance; }
|
||||
void SetCamera2DPlaneMaxDrawingDistance(double distance) { camera2DPlaneMaxDrawingDistance = distance; }
|
||||
///@}
|
||||
|
||||
/** \name Cameras
|
||||
@@ -292,6 +294,7 @@ class GD_CORE_API Layer {
|
||||
double camera3DNearPlaneDistance; ///< 3D camera frustum near plan distance
|
||||
double camera3DFarPlaneDistance; ///< 3D camera frustum far plan distance
|
||||
double camera3DFieldOfView; ///< 3D camera field of view (fov) in degrees
|
||||
double camera2DPlaneMaxDrawingDistance; ///< Max drawing distance of the 2D plane when in the 3D world
|
||||
unsigned int ambientLightColorR; ///< Ambient light color Red component
|
||||
unsigned int ambientLightColorG; ///< Ambient light color Green component
|
||||
unsigned int ambientLightColorB; ///< Ambient light color Blue component
|
||||
|
@@ -730,6 +730,8 @@ void Project::UnserializeFrom(const SerializerElement& element) {
|
||||
SetPackageName(propElement.GetStringAttribute("packageName"));
|
||||
SetTemplateSlug(propElement.GetStringAttribute("templateSlug"));
|
||||
SetOrientation(propElement.GetStringAttribute("orientation", "default"));
|
||||
SetEffectsHiddenInEditor(
|
||||
propElement.GetBoolAttribute("areEffectsHiddenInEditor", false));
|
||||
SetFolderProject(propElement.GetBoolAttribute("folderProject"));
|
||||
SetLastCompilationDirectory(propElement
|
||||
.GetChild("latestCompilationDirectory",
|
||||
@@ -1109,6 +1111,10 @@ void Project::SerializeTo(SerializerElement& element) const {
|
||||
propElement.SetAttribute("packageName", packageName);
|
||||
propElement.SetAttribute("templateSlug", templateSlug);
|
||||
propElement.SetAttribute("orientation", orientation);
|
||||
if (areEffectsHiddenInEditor) {
|
||||
propElement.SetBoolAttribute("areEffectsHiddenInEditor",
|
||||
areEffectsHiddenInEditor);
|
||||
}
|
||||
platformSpecificAssets.SerializeTo(
|
||||
propElement.AddChild("platformSpecificAssets"));
|
||||
loadingScreen.SerializeTo(propElement.AddChild("loadingScreen"));
|
||||
@@ -1150,6 +1156,8 @@ void Project::SerializeTo(SerializerElement& element) const {
|
||||
// end of compatibility code
|
||||
|
||||
extensionProperties.SerializeTo(propElement.AddChild("extensionProperties"));
|
||||
|
||||
playableDevicesElement.AddChild("").SetStringValue("mobile");
|
||||
|
||||
SerializerElement& platformsElement = propElement.AddChild("platforms");
|
||||
platformsElement.ConsiderAsArrayOf("platform");
|
||||
@@ -1319,6 +1327,8 @@ void Project::Init(const gd::Project& game) {
|
||||
|
||||
sceneResourcesPreloading = game.sceneResourcesPreloading;
|
||||
sceneResourcesUnloading = game.sceneResourcesUnloading;
|
||||
|
||||
areEffectsHiddenInEditor = game.areEffectsHiddenInEditor;
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -506,6 +506,20 @@ class GD_CORE_API Project {
|
||||
*/
|
||||
void SetCurrentPlatform(const gd::String& platformName);
|
||||
|
||||
/**
|
||||
* Check if the effects are shown.
|
||||
*/
|
||||
bool AreEffectsHiddenInEditor() const { return areEffectsHiddenInEditor; }
|
||||
|
||||
/**
|
||||
* Define the project as playable on a mobile.
|
||||
* \param enable True When false effects are not shown and a default light is
|
||||
* used for 3D layers.
|
||||
*/
|
||||
void SetEffectsHiddenInEditor(bool enable = true) {
|
||||
areEffectsHiddenInEditor = enable;
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Factory method
|
||||
@@ -1165,6 +1179,9 @@ class GD_CORE_API Project {
|
||||
mutable unsigned int gdBuildVersion =
|
||||
0; ///< The GD build version used the last
|
||||
///< time the project was saved.
|
||||
bool areEffectsHiddenInEditor =
|
||||
false; ///< When false effects are not shown and a default light is used
|
||||
///< for 3D layers.
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -70,7 +70,9 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
currentValue = element.GetChild("value").GetStringValue();
|
||||
type = element.GetChild("type").GetStringValue();
|
||||
if (type == "Number") {
|
||||
gd::String unitName = element.GetChild("unit").GetStringValue();
|
||||
gd::String unitName = element.HasChild("unit")
|
||||
? element.GetChild("unit").GetStringValue()
|
||||
: "";
|
||||
measurementUnit =
|
||||
gd::MeasurementUnit::HasDefaultMeasurementUnitNamed(unitName)
|
||||
? measurementUnit =
|
||||
|
@@ -159,15 +159,9 @@ namespace gdjs {
|
||||
if (initialInstanceData.depth !== undefined) {
|
||||
this.setDepth(initialInstanceData.depth);
|
||||
}
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
if (initialInstanceData.flippedZ) {
|
||||
this.flipZ(initialInstanceData.flippedZ);
|
||||
}
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
this.flipZ(!!initialInstanceData.flippedZ);
|
||||
}
|
||||
|
||||
setX(x: float): void {
|
||||
@@ -334,6 +328,18 @@ namespace gdjs {
|
||||
this.setAngle(gdjs.toDegrees(mesh.rotation.z));
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this._originalWidth;
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this._originalHeight;
|
||||
}
|
||||
|
||||
getOriginalDepth(): float {
|
||||
return this._originalDepth;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._width;
|
||||
}
|
||||
@@ -380,31 +386,6 @@ namespace gdjs {
|
||||
this.getRenderer().updateSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the object for a scale of 1.
|
||||
*
|
||||
* It can't be 0.
|
||||
*/
|
||||
_getOriginalWidth(): float {
|
||||
return this._originalWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the height of the object for a scale of 1.
|
||||
*
|
||||
* It can't be 0.
|
||||
*/
|
||||
_getOriginalHeight(): float {
|
||||
return this._originalHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object size on the Z axis (called "depth") when the scale equals 1.
|
||||
*/
|
||||
_getOriginalDepth(): float {
|
||||
return this._originalDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the object for a scale of 1.
|
||||
*/
|
||||
|
@@ -11,6 +11,8 @@ namespace gdjs {
|
||||
this._object = runtimeObject;
|
||||
this._threeObject3D = threeObject3D;
|
||||
this._threeObject3D.rotation.order = 'ZYX';
|
||||
//@ts-ignore
|
||||
this._threeObject3D.gdjsRuntimeObject = runtimeObject;
|
||||
|
||||
instanceContainer
|
||||
.getLayer('')
|
||||
|
@@ -115,6 +115,12 @@ namespace gdjs {
|
||||
* Rotations around X and Y are not taken into account.
|
||||
*/
|
||||
getUnrotatedAABBMaxZ(): number;
|
||||
|
||||
/**
|
||||
* Return the depth of the object before any custom size is applied.
|
||||
* @return The depth of the object
|
||||
*/
|
||||
getOriginalDepth(): float;
|
||||
}
|
||||
|
||||
export interface Object3DDataContent {
|
||||
@@ -131,7 +137,11 @@ namespace gdjs {
|
||||
export namespace Base3DHandler {
|
||||
export const is3D = (
|
||||
object: gdjs.RuntimeObject
|
||||
): object is gdjs.RuntimeObject & gdjs.Base3DHandler => {
|
||||
): object is gdjs.RuntimeObject &
|
||||
gdjs.Base3DHandler &
|
||||
gdjs.Resizable &
|
||||
gdjs.Scalable &
|
||||
gdjs.Flippable => {
|
||||
//@ts-ignore We are checking if the methods are present.
|
||||
return object.getZ && object.setZ;
|
||||
};
|
||||
@@ -243,6 +253,10 @@ namespace gdjs {
|
||||
getUnrotatedAABBMaxZ(): number {
|
||||
return this.object.getUnrotatedAABBMaxZ();
|
||||
}
|
||||
|
||||
getOriginalDepth(): float {
|
||||
return this.object.getOriginalDepth();
|
||||
}
|
||||
}
|
||||
|
||||
gdjs.registerBehavior('Scene3D::Base3DBehavior', gdjs.Base3DBehavior);
|
||||
|
@@ -78,15 +78,9 @@ namespace gdjs {
|
||||
if (initialInstanceData.depth !== undefined) {
|
||||
this.setDepth(initialInstanceData.depth);
|
||||
}
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
if (initialInstanceData.flippedZ) {
|
||||
this.flipZ(initialInstanceData.flippedZ);
|
||||
}
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
this.flipZ(!!initialInstanceData.flippedZ);
|
||||
}
|
||||
|
||||
getNetworkSyncData(
|
||||
@@ -325,6 +319,10 @@ namespace gdjs {
|
||||
return this._maxZ - this._minZ;
|
||||
}
|
||||
|
||||
getOriginalDepth(): float {
|
||||
return this._instanceContainer._getInitialInnerAreaDepth();
|
||||
}
|
||||
|
||||
override _updateUntransformedHitBoxes(): void {
|
||||
super._updateUntransformedHitBoxes();
|
||||
|
||||
|
@@ -23,6 +23,8 @@ namespace gdjs {
|
||||
|
||||
this._threeGroup = new THREE.Group();
|
||||
this._threeGroup.rotation.order = 'ZYX';
|
||||
//@ts-ignore
|
||||
this._threeGroup.gdjsRuntimeObject = object;
|
||||
|
||||
const layer = parent.getLayer('');
|
||||
if (layer) {
|
||||
|
@@ -40,6 +40,19 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -221,9 +221,11 @@ namespace gdjs {
|
||||
this.setWrappingWidth(initialInstanceData.width);
|
||||
this.setWrapping(true);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
override onDestroyed(): void {
|
||||
|
@@ -29,6 +29,19 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -33,49 +33,6 @@ namespace gdjs {
|
||||
claimSecret?: string;
|
||||
};
|
||||
|
||||
// Rolling window rate limiting
|
||||
// Implements rate limiting to prevent abuse:
|
||||
// - Maximum 12 successful successful entries per minute across all leaderboards
|
||||
// - Maximum 6 successful successful entries per minute per individual leaderboard
|
||||
// - Works in addition to existing 500ms cooldown between entry tentatives
|
||||
let _successfulEntriesGlobal: number[] = []; // Timestamps of successful entries across all leaderboards
|
||||
|
||||
const GLOBAL_RATE_LIMIT_COUNT = 12;
|
||||
const PER_LEADERBOARD_RATE_LIMIT_COUNT = 6;
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute in milliseconds
|
||||
|
||||
/**
|
||||
* Clean old entries from the rolling window (older than 1 minute)
|
||||
*/
|
||||
const cleanOldEntries = (
|
||||
entries: number[],
|
||||
currentTime: number
|
||||
): number[] => {
|
||||
return entries.filter(
|
||||
(timestamp) => currentTime - timestamp < RATE_LIMIT_WINDOW_MS
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if adding a new entry would exceed global rate limits.
|
||||
*/
|
||||
const wouldExceedGlobalSuccessRateLimit = (): boolean => {
|
||||
const currentTime = Date.now();
|
||||
_successfulEntriesGlobal = cleanOldEntries(
|
||||
_successfulEntriesGlobal,
|
||||
currentTime
|
||||
);
|
||||
return _successfulEntriesGlobal.length >= GLOBAL_RATE_LIMIT_COUNT;
|
||||
};
|
||||
|
||||
/**
|
||||
* Record a successful entry for global rate limiting tracking.
|
||||
*/
|
||||
const recordGlobalSuccessfulEntry = (): void => {
|
||||
const currentTime = Date.now();
|
||||
_successfulEntriesGlobal.push(currentTime);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hold the state of the save of a score for a leaderboard.
|
||||
*/
|
||||
@@ -86,9 +43,6 @@ namespace gdjs {
|
||||
/** The promise that will be resolved when the score saving is done (successfully or not). */
|
||||
lastSavingPromise: Promise<void> | null = null;
|
||||
|
||||
/** Timestamps of successful entries for this leaderboard (for rate limiting) */
|
||||
private _successfulEntries: number[] = [];
|
||||
|
||||
// Score that is being saved:
|
||||
private _currentlySavingScore: number | null = null;
|
||||
private _currentlySavingPlayerName: string | null = null;
|
||||
@@ -153,36 +107,13 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
private _wouldExceedPerLeaderboardTentativeRateLimit(): boolean {
|
||||
// Prevent entries within 500ms of each other (per leaderboard)
|
||||
// as this would indicate surely a score saved every frame.
|
||||
//
|
||||
// Note that is on lastScoreSavingStartedAt, not lastScoreSavingSucceededAt,
|
||||
// which means we limit tentatives here (and not successes).
|
||||
private _isTooSoonToSaveAnotherScore(): boolean {
|
||||
return (
|
||||
!!this.lastScoreSavingStartedAt &&
|
||||
Date.now() - this.lastScoreSavingStartedAt < 500
|
||||
);
|
||||
}
|
||||
|
||||
private _wouldExceedPerLeaderboardSuccessRateLimit(): boolean {
|
||||
const currentTime = Date.now();
|
||||
this._successfulEntries = cleanOldEntries(
|
||||
this._successfulEntries,
|
||||
currentTime
|
||||
);
|
||||
return (
|
||||
this._successfulEntries.length >= PER_LEADERBOARD_RATE_LIMIT_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
private _recordPerLeaderboardAndGlobalSuccessfulEntry(): void {
|
||||
const currentTime = Date.now();
|
||||
this._successfulEntries.push(currentTime);
|
||||
|
||||
recordGlobalSuccessfulEntry();
|
||||
}
|
||||
|
||||
startSaving({
|
||||
playerName,
|
||||
playerId,
|
||||
@@ -210,7 +141,7 @@ namespace gdjs {
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
if (this._wouldExceedPerLeaderboardTentativeRateLimit()) {
|
||||
if (this._isTooSoonToSaveAnotherScore()) {
|
||||
logger.warn(
|
||||
'Last entry was sent too little time ago. Ignoring this one.'
|
||||
);
|
||||
@@ -223,24 +154,6 @@ namespace gdjs {
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
// Rolling window rate limiting check for successful entries.
|
||||
if (wouldExceedGlobalSuccessRateLimit()) {
|
||||
logger.warn(
|
||||
'Rate limit exceeded. Too many entries have been successfully sent recently across all leaderboards. Ignoring this one.'
|
||||
);
|
||||
this._setError('GLOBAL_RATE_LIMIT_EXCEEDED');
|
||||
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
if (this._wouldExceedPerLeaderboardSuccessRateLimit()) {
|
||||
logger.warn(
|
||||
'Rate limit exceeded. Too many entries have been successfully sent recently for this leaderboard. Ignoring this one.'
|
||||
);
|
||||
this._setError('LEADERBOARD_RATE_LIMIT_EXCEEDED');
|
||||
|
||||
throw new Error('Ignoring this saving request.');
|
||||
}
|
||||
|
||||
let resolveSavingPromise: () => void;
|
||||
const savingPromise = new Promise<void>((resolve) => {
|
||||
resolveSavingPromise = resolve;
|
||||
@@ -256,9 +169,6 @@ namespace gdjs {
|
||||
|
||||
return {
|
||||
closeSaving: (leaderboardEntry) => {
|
||||
// Record successful entry for rolling window rate limiting.
|
||||
this._recordPerLeaderboardAndGlobalSuccessfulEntry();
|
||||
|
||||
if (savingPromise !== this.lastSavingPromise) {
|
||||
logger.info(
|
||||
'Score saving result received, but another save was launched in the meantime - ignoring the result of this one.'
|
||||
@@ -478,6 +388,9 @@ namespace gdjs {
|
||||
|
||||
return new gdjs.PromiseTask(
|
||||
(async () => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
const scoreSavingState = (_scoreSavingStateByLeaderboard[
|
||||
leaderboardId
|
||||
] =
|
||||
@@ -486,10 +399,7 @@ namespace gdjs {
|
||||
|
||||
try {
|
||||
const { closeSaving, closeSavingWithError } =
|
||||
scoreSavingState.startSaving({
|
||||
playerName,
|
||||
score,
|
||||
});
|
||||
scoreSavingState.startSaving({ playerName, score });
|
||||
|
||||
try {
|
||||
const leaderboardEntry = await saveScore({
|
||||
@@ -516,6 +426,9 @@ namespace gdjs {
|
||||
) =>
|
||||
new gdjs.PromiseTask(
|
||||
(async () => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (!playerId || !playerToken) {
|
||||
@@ -533,10 +446,7 @@ namespace gdjs {
|
||||
|
||||
try {
|
||||
const { closeSaving, closeSavingWithError } =
|
||||
scoreSavingState.startSaving({
|
||||
playerId,
|
||||
score,
|
||||
});
|
||||
scoreSavingState.startSaving({ playerId, score });
|
||||
|
||||
try {
|
||||
const leaderboardEntryId = await saveScore({
|
||||
@@ -843,6 +753,9 @@ namespace gdjs {
|
||||
leaderboardId: string,
|
||||
displayLoader: boolean
|
||||
) {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
// First ensure we're not trying to display multiple times the same leaderboard (in which case
|
||||
// we "de-duplicate" the request to display it).
|
||||
if (leaderboardId === _requestedLeaderboardId) {
|
||||
|
@@ -35,6 +35,19 @@ describe('gdjs.LinksManager', function () {
|
||||
stopSoundsOnStartup: false,
|
||||
title: '',
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -1841,6 +1841,9 @@ namespace gdjs {
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
if (isQuickJoiningTooFast()) {
|
||||
return;
|
||||
}
|
||||
@@ -1860,6 +1863,9 @@ namespace gdjs {
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
if (isQuickJoiningTooFast()) {
|
||||
return;
|
||||
}
|
||||
@@ -1893,6 +1899,9 @@ namespace gdjs {
|
||||
export const openLobbiesWindow = async (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isLobbiesWindowOpen(runtimeScene) ||
|
||||
gdjs.playerAuthentication.isAuthenticationWindowOpen()
|
||||
|
@@ -53,6 +53,8 @@ namespace gdjs {
|
||||
|
||||
_renderer: gdjs.PanelSpriteRuntimeObjectRenderer;
|
||||
|
||||
_objectData: PanelSpriteObjectData;
|
||||
|
||||
/**
|
||||
* @param instanceContainer The container the object belongs to.
|
||||
* @param panelSpriteObjectData The initial properties of the object
|
||||
@@ -62,6 +64,7 @@ namespace gdjs {
|
||||
panelSpriteObjectData: PanelSpriteObjectData
|
||||
) {
|
||||
super(instanceContainer, panelSpriteObjectData);
|
||||
this._objectData = panelSpriteObjectData;
|
||||
this._rBorder = panelSpriteObjectData.rightMargin;
|
||||
this._lBorder = panelSpriteObjectData.leftMargin;
|
||||
this._tBorder = panelSpriteObjectData.topMargin;
|
||||
@@ -84,6 +87,7 @@ namespace gdjs {
|
||||
oldObjectData: PanelSpriteObjectData,
|
||||
newObjectData: PanelSpriteObjectData
|
||||
): boolean {
|
||||
this._objectData = newObjectData;
|
||||
if (oldObjectData.width !== newObjectData.width) {
|
||||
this.setWidth(newObjectData.width);
|
||||
}
|
||||
@@ -166,9 +170,11 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,6 +253,14 @@ namespace gdjs {
|
||||
this.setHeight(newHeight);
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this._objectData.width;
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this._objectData.height;
|
||||
}
|
||||
|
||||
setOpacity(opacity: float): void {
|
||||
if (opacity < 0) {
|
||||
opacity = 0;
|
||||
|
@@ -18,12 +18,15 @@ namespace gdjs {
|
||||
renderer: PIXI.Container;
|
||||
emitter: PIXI.particles.Emitter;
|
||||
started: boolean = false;
|
||||
helperGraphics: PIXI.Graphics | null = null;
|
||||
runtimeObject: gdjs.ParticleEmitterObject;
|
||||
|
||||
constructor(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
runtimeObject: gdjs.RuntimeObject,
|
||||
runtimeObject: gdjs.ParticleEmitterObject,
|
||||
objectData: any
|
||||
) {
|
||||
this.runtimeObject = runtimeObject;
|
||||
const pixiRenderer = instanceContainer
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
@@ -223,6 +226,44 @@ namespace gdjs {
|
||||
if (!this.started && wasEmitting) {
|
||||
this.started = true;
|
||||
}
|
||||
if (this.helperGraphics) {
|
||||
this.helperGraphics.clear();
|
||||
this.helperGraphics.position.x = this.runtimeObject.getX();
|
||||
this.helperGraphics.position.y = this.runtimeObject.getY();
|
||||
|
||||
const emitterAngle = gdjs.toRad(this.runtimeObject.getAngle());
|
||||
const sprayConeAngle = gdjs.toRad(
|
||||
this.runtimeObject.getConeSprayAngle()
|
||||
);
|
||||
const line1Angle = emitterAngle - sprayConeAngle / 2;
|
||||
const line2Angle = emitterAngle + sprayConeAngle / 2;
|
||||
const length = 64;
|
||||
|
||||
this.helperGraphics.beginFill(0, 0);
|
||||
this.helperGraphics.lineStyle(
|
||||
3,
|
||||
this.runtimeObject.getParticleColorEnd(),
|
||||
1
|
||||
);
|
||||
this.helperGraphics.moveTo(0, 0);
|
||||
this.helperGraphics.lineTo(
|
||||
Math.cos(line1Angle) * length,
|
||||
Math.sin(line1Angle) * length
|
||||
);
|
||||
this.helperGraphics.moveTo(0, 0);
|
||||
this.helperGraphics.lineTo(
|
||||
Math.cos(line2Angle) * length,
|
||||
Math.sin(line2Angle) * length
|
||||
);
|
||||
this.helperGraphics.endFill();
|
||||
|
||||
this.helperGraphics.lineStyle(0, 0x000000, 1);
|
||||
this.helperGraphics.beginFill(
|
||||
this.runtimeObject.getParticleColorStart()
|
||||
);
|
||||
this.helperGraphics.drawCircle(0, 0, 8);
|
||||
this.helperGraphics.endFill();
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(x: number, y: number): void {
|
||||
@@ -443,6 +484,17 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
private static readonly frequencyMinimumValue = 0.0001;
|
||||
|
||||
setHelperVisible(visible: boolean) {
|
||||
if (visible && !this.helperGraphics) {
|
||||
this.helperGraphics = new PIXI.Graphics();
|
||||
this.renderer.addChild(this.helperGraphics);
|
||||
} else if (!visible && this.helperGraphics) {
|
||||
this.helperGraphics.removeFromParent();
|
||||
this.helperGraphics.destroy();
|
||||
this.helperGraphics = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore - Register the class to let the engine use it.
|
||||
|
@@ -174,6 +174,10 @@ namespace gdjs {
|
||||
this,
|
||||
particleObjectData
|
||||
);
|
||||
if (instanceContainer.getGame().isInGameEdition()) {
|
||||
// TODO Disable the particles rendering
|
||||
this._renderer.setHelperVisible(true);
|
||||
}
|
||||
this.angleA = particleObjectData.emitterAngleA;
|
||||
this.angleB = particleObjectData.emitterAngleB;
|
||||
this.forceMin = particleObjectData.emitterForceMin;
|
||||
@@ -802,6 +806,14 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
getParticleColorStart(): number {
|
||||
return this.color1;
|
||||
}
|
||||
|
||||
getParticleColorEnd(): number {
|
||||
return this.color2;
|
||||
}
|
||||
|
||||
getParticleRed1(): number {
|
||||
return gdjs.hexNumberToRGBArray(this.color1)[0];
|
||||
}
|
||||
|
@@ -36,6 +36,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -39,6 +39,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -41,6 +41,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -623,10 +623,46 @@ Zv();a.b2Manifold.e_faceA=$v();a.b2Manifold.e_faceB=aw();a.b2_staticBody=bw();a.
|
||||
})();
|
||||
|
||||
gdjs.registerAsynchronouslyLoadingLibraryPromise(initializeBox2D({locateFile: function(path, prefix) {
|
||||
return location.protocol === 'file:' ?
|
||||
// This is needed to run on Electron.
|
||||
prefix + "Extensions/Physics2Behavior/" + path :
|
||||
prefix + path;
|
||||
// Path should always be "Box2D_v2.3.1_min.wasm.wasm" (and if it's not, we should probably hardcode it).
|
||||
if (path !== 'Box2D_v2.3.1_min.wasm.wasm') {
|
||||
console.warn("'path' argument sent to locateFile in Box2D_v2.3.1_min.wasm.js is not the expected string 'Box2D_v2.3.1_min.wasm.wasm'. Loading may fail.")
|
||||
}
|
||||
|
||||
// Prefix is typically:
|
||||
// Games ("exported", standalone game):
|
||||
// - Web game: "https://games.gdevelop-app.com/[...]/Extensions/Physics2Behavior/"
|
||||
// - Cordova Android: "https://localhost/Extensions/Physics2Behavior/".
|
||||
// - Cordova iOS: "ionic://localhost/Extensions/Physics2Behavior/".
|
||||
// - Electron macOS: "/private/var/[...]/Contents/Resources/app.asar/app/" (notice the missing folder).
|
||||
// - Electron Windows: "C:\Users\[...]\AppData\Local\[...]\resources\app.asar\app/" (notice the missing folder).
|
||||
// Preview (in the editor):
|
||||
// - Web app preview (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
|
||||
// - Web app preview (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
|
||||
// - Electron app preview (dev editor): "/var/[...]/preview/" (notice the missing folder).
|
||||
// - Electron app preview (production editor): "/var/[...]/preview/" (notice the missing folder).
|
||||
// In-game editor:
|
||||
// - Web app (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
|
||||
// - Web app (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
|
||||
// - Electron app (dev editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
|
||||
// - Electron app (production editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
|
||||
|
||||
// If the prefix is a full URL, it's a full URL to the folder containing this JS file.
|
||||
// Sill consider the case where the folder could have been missing.
|
||||
let url;
|
||||
if (prefix.startsWith('http:') || prefix.startsWith('https:')) {
|
||||
url = prefix.endsWith('Extensions/Physics2Behavior/') ?
|
||||
prefix + path :
|
||||
prefix + 'Extensions/Physics2Behavior/' + path;
|
||||
} else {
|
||||
// Electron or Cordova iOS will fall in this case.
|
||||
// We can't use this simple solution for http/https because
|
||||
// on the web-app, the runtime is not necessarily hosted
|
||||
// on the same domain as where the game generated files are served (so "prefix" is needed).
|
||||
url = "Extensions/Physics2Behavior/" + path;
|
||||
}
|
||||
|
||||
console.info(`Box2D wasm file is being loaded from path "${path}" with prefix "${prefix}". Resolved URL: "${url}".`);
|
||||
return url;
|
||||
}}).then(box2d => {
|
||||
window.Box2D = box2d;
|
||||
}));
|
||||
|
@@ -647,6 +647,9 @@ namespace gdjs {
|
||||
export const displayAuthenticationBanner = function (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
if (_authenticationBanner) {
|
||||
// Banner already displayed, ensure it's visible.
|
||||
_authenticationBanner.style.opacity = '1';
|
||||
@@ -1042,6 +1045,10 @@ namespace gdjs {
|
||||
): gdjs.PromiseTask<{ status: 'logged' | 'errored' | 'dismissed' }> =>
|
||||
new gdjs.PromiseTask(
|
||||
new Promise((resolve) => {
|
||||
if (runtimeScene.getGame().isInGameEdition()) {
|
||||
resolve({ status: 'dismissed' });
|
||||
}
|
||||
|
||||
// Create the authentication container for the player to wait.
|
||||
const domElementContainer = runtimeScene
|
||||
.getGame()
|
||||
|
@@ -20,6 +20,8 @@ namespace gdjs {
|
||||
|
||||
_antialiasingFilter: null | PIXI.Filter = null;
|
||||
|
||||
_placeholder: PIXI.Sprite | null = null;
|
||||
|
||||
private static readonly _positionForTransformation: PIXI.IPointData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -405,6 +407,25 @@ namespace gdjs {
|
||||
|
||||
updatePreRender(): void {
|
||||
this.updatePositionIfNeeded();
|
||||
|
||||
const game = this._object.getRuntimeScene().getGame();
|
||||
if (
|
||||
game.isInGameEdition() &&
|
||||
this._graphics.geometry.graphicsData.length === 0
|
||||
) {
|
||||
if (!this._placeholder) {
|
||||
console.log(game.getGameData().resources.resources);
|
||||
const texture = game
|
||||
.getImageManager()
|
||||
.getPIXITexture('InGameEditor-ShapePainterIcon');
|
||||
this._placeholder = new PIXI.Sprite(texture);
|
||||
}
|
||||
this._graphics.addChild(this._placeholder);
|
||||
} else if (this._placeholder) {
|
||||
this._placeholder.removeFromParent();
|
||||
this._placeholder.destroy();
|
||||
this._placeholder = null;
|
||||
}
|
||||
}
|
||||
|
||||
updatePositionX(): void {
|
||||
|
@@ -278,12 +278,8 @@ namespace gdjs {
|
||||
* @param initialInstanceData The extra parameters
|
||||
*/
|
||||
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
}
|
||||
|
||||
stepBehaviorsPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
||||
|
@@ -50,6 +50,19 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets
|
||||
instances: [],
|
||||
variables: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -21,9 +21,7 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'SaveState',
|
||||
_('Save State (experimental)'),
|
||||
_(
|
||||
'Allows to save and load the full state of a game, usually on the device storage. A Save State, by default, contains the full state of the game (objects, variables, sounds, music, effects etc.). Using the "Save Configuration" behavior, you can customize which objects should not be saved in a Save State. You can also use the "Change the save configuration of a variable" action to change the save configuration of a variable. Finally, both objects, variables and scene/game data can be given a profile name: in this case, when saving or loading with one or more profile names specified, only the object/variables/data belonging to one of the specified profiles will be saved or loaded.'
|
||||
),
|
||||
_('Allows to save and load the full state of a game.'),
|
||||
'Neyl Mahfouf',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
@@ -34,135 +32,65 @@ module.exports = {
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'CreateGameSaveStateInVariable',
|
||||
'SaveGameSnapshotToVariable',
|
||||
_('Save game to a variable'),
|
||||
_(
|
||||
'Create a Save State and save it to a variable. This is for advanced usage, prefer to use "Save game to device storage" in most cases.'
|
||||
),
|
||||
_('Save game in variable _PARAM1_ (profile(s): _PARAM2_)'),
|
||||
_('Takes a snapshot of the game and save it to a variable.'),
|
||||
_('Save the game in variable _PARAM1_'),
|
||||
_('Save'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('variable', _('Variable to store the save to'), '', false)
|
||||
.addParameter('string', _('Profile(s) to save'), '', true)
|
||||
.setDefaultValue('"default"')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.createGameSaveStateInVariable');
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.saveVariableGameSnapshot');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'CreateGameSaveStateInStorage',
|
||||
'SaveGameSnapshotToStorage',
|
||||
_('Save game to device storage'),
|
||||
_('Create a Save State and save it to device storage.'),
|
||||
_('Save game to device storage named _PARAM1_ (profile(s): _PARAM2_)'),
|
||||
_('Takes a snapshot of the game and save it to device storage.'),
|
||||
_('Save the game to device storage under key _PARAM1_'),
|
||||
_('Save'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('string', _('Storage key to save to'), '', false)
|
||||
.addParameter('string', _('Profile(s) to save'), '', true)
|
||||
.setDefaultValue('"default"')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
|
||||
)
|
||||
)
|
||||
.setDefaultValue('no')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.createGameSaveStateInStorage');
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.saveStorageGameSnapshot');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'RestoreGameSaveStateFromVariable',
|
||||
'LoadGameSnapshotFromVariable',
|
||||
_('Load game from variable'),
|
||||
_(
|
||||
'Restore the game from a Save State stored in the specified variable. This is for advanced usage, prefer to use "Load game from device storage" in most cases.'
|
||||
),
|
||||
_(
|
||||
'Load game from variable _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
|
||||
),
|
||||
_('Load game from a variable save snapshot.'),
|
||||
_('Load the game from variable _PARAM0_'),
|
||||
_('Load'),
|
||||
'res/actions/saveUp.svg',
|
||||
'res/actions/saveUp.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('variable', _('Variable to load the game from'), '', false)
|
||||
.addParameter('string', _('Profile(s) to load'), '', true)
|
||||
.setDefaultValue('"default"')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded (unless they have a "Save Configuration" behavior set to "Do not save").'
|
||||
)
|
||||
)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Stop and restart all the scenes currently played?'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('no')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromVariable');
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.loadGameFromVariableSnapshot');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'RestoreGameSaveStateFromStorage',
|
||||
'LoadGameSnapshotFromStorage',
|
||||
_('Load game from device storage'),
|
||||
_('Restore the game from a Save State stored on the device.'),
|
||||
_(
|
||||
'Load game from device storage named _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
|
||||
),
|
||||
_('Load game from device storage save snapshot.'),
|
||||
_('Load the game from device storage under key _PARAM0_.'),
|
||||
_('Load'),
|
||||
'res/actions/saveUp.svg',
|
||||
'res/actions/saveUp.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter(
|
||||
'string',
|
||||
_('Storage name to load the game from'),
|
||||
'',
|
||||
false
|
||||
)
|
||||
.addParameter('string', _('Profile(s) to load'), '', true)
|
||||
.setDefaultValue('"default"')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded.'
|
||||
)
|
||||
)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Stop and restart all the scenes currently played?'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('no')
|
||||
.addParameter('string', _('Storage key to load the game from'), '', false)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromStorage');
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.loadGameFromStorageSnapshot');
|
||||
|
||||
extension
|
||||
.addExpressionAndCondition(
|
||||
@@ -176,12 +104,8 @@ module.exports = {
|
||||
'',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.getSecondsSinceLastSave')
|
||||
.setGetter('gdjs.saveState.getSecondsSinceLastSave');
|
||||
|
||||
@@ -197,12 +121,8 @@ module.exports = {
|
||||
'',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.getSecondsSinceLastLoad')
|
||||
.setGetter('gdjs.saveState.getSecondsSinceLastLoad');
|
||||
|
||||
@@ -210,276 +130,58 @@ module.exports = {
|
||||
.addCondition(
|
||||
'SaveJustSucceeded',
|
||||
_('Save just succeeded'),
|
||||
_('The last save attempt just succeeded.'),
|
||||
_('Save just succeeded'),
|
||||
_('the save just succeeded'),
|
||||
_('the save just succeeded'),
|
||||
_('Save'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.hasSaveJustSucceeded');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'SaveJustFailed',
|
||||
_('Save just failed'),
|
||||
_('The last save attempt just failed.'),
|
||||
_('Save just failed'),
|
||||
_('the save just failed'),
|
||||
_('the save just failed'),
|
||||
_('Save'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.hasSaveJustFailed');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'LoadJustSucceeded',
|
||||
_('Load just succeeded'),
|
||||
_('The last load attempt just succeeded.'),
|
||||
_('Load just succeeded'),
|
||||
_('the load just succeeded'),
|
||||
_('the load just succeeded'),
|
||||
_('Load'),
|
||||
'res/actions/saveUp.svg',
|
||||
'res/actions/saveUp.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.hasLoadJustSucceeded');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'LoadJustFailed',
|
||||
_('Load just failed'),
|
||||
_('The last load attempt just failed.'),
|
||||
_('Load just failed'),
|
||||
_('the load just failed'),
|
||||
_('the load just failed'),
|
||||
_('Load'),
|
||||
'res/actions/saveUp.svg',
|
||||
'res/actions/saveUp.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setIncludeFile('Extensions/SaveState/savestatetools.js')
|
||||
.setFunctionName('gdjs.saveState.hasLoadJustFailed');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'SetVariableSaveConfiguration',
|
||||
_('Change the save configuration of a variable'),
|
||||
_(
|
||||
'Set if a scene or global variable should be saved in the default save state. Also allow to specify one or more profiles in which the variable should be saved.'
|
||||
),
|
||||
_(
|
||||
'Change save configuration of _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
|
||||
),
|
||||
_('Advanced configuration'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter(
|
||||
'variable',
|
||||
_('Variable for which configuration should be changed'),
|
||||
'',
|
||||
false
|
||||
)
|
||||
.addParameter('yesorno', _('Persist in default save states'), '', false)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter(
|
||||
'string',
|
||||
_('Profiles in which the variable should be saved'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names in which the variable will be saved. When a save state is created with one or more profile names specified, the variable will be saved only if it matches one of these profiles.'
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.setVariableSaveConfiguration');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'SetGameDataSaveConfiguration',
|
||||
_('Change the save configuration of the global game data'),
|
||||
_(
|
||||
'Set if the global game data (audio & global variables) should be saved in the default save state. Also allow to specify one or more profiles in which the global game data should be saved.'
|
||||
),
|
||||
_(
|
||||
'Change save configuration of global game data: save them in the default save states: _PARAM1_ and in profiles: _PARAM2_'
|
||||
),
|
||||
_('Advanced configuration'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('yesorno', _('Persist in default save states'), '', false)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter(
|
||||
'string',
|
||||
_('Profiles in which the global game data should be saved'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names in which the global game data will be saved. When a save state is created with one or more profile names specified, the global game data will be saved only if it matches one of these profiles.'
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.setGameDataSaveConfiguration');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'SetSceneDataSaveConfiguration',
|
||||
_('Change the save configuration of a scene data'),
|
||||
_(
|
||||
'Set if the data of the specified scene (scene variables, timers, trigger once, wait actions, layers, etc.) should be saved in the default save state. Also allow to specify one or more profiles in which the scene data should be saved. Note: objects are always saved separately from the scene data (use the "Save Configuration" behavior to customize the configuration of objects).'
|
||||
),
|
||||
_(
|
||||
'Change save configuration of scene _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
|
||||
),
|
||||
_('Advanced configuration'),
|
||||
'res/actions/saveDown.svg',
|
||||
'res/actions/saveDown.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter(
|
||||
'sceneName',
|
||||
_('Scene name for which configuration should be changed'),
|
||||
'',
|
||||
false
|
||||
)
|
||||
.addParameter('yesorno', _('Persist in default save states'), '', false)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter(
|
||||
'string',
|
||||
_('Profiles in which the scene data should be saved'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('')
|
||||
.setParameterLongDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names in which the scene data will be saved. When a save state is created with one or more profile names specified, the scene data will be saved only if it matches one of these profiles.'
|
||||
)
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
)
|
||||
.setFunctionName('gdjs.saveState.setSceneDataSaveConfiguration');
|
||||
|
||||
// Save Configuration behavior
|
||||
const saveConfigurationBehavior = new gd.BehaviorJsImplementation();
|
||||
|
||||
saveConfigurationBehavior.updateProperty = function (
|
||||
behaviorContent,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'defaultProfilePersistence') {
|
||||
behaviorContent
|
||||
.getChild('defaultProfilePersistence')
|
||||
.setStringValue(newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'persistedInProfiles') {
|
||||
behaviorContent
|
||||
.getChild('persistedInProfiles')
|
||||
.setStringValue(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
saveConfigurationBehavior.getProperties = function (behaviorContent) {
|
||||
const behaviorProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
behaviorProperties
|
||||
.getOrCreate('defaultProfilePersistence')
|
||||
.setValue(
|
||||
behaviorContent.getChild('defaultProfilePersistence').getStringValue()
|
||||
)
|
||||
.setType('Choice')
|
||||
.setLabel(_('Persistence mode'))
|
||||
.addChoice('Persisted', _('Include in save states (default)'))
|
||||
.addChoice('DoNotSave', _('Do not save'));
|
||||
|
||||
behaviorProperties
|
||||
.getOrCreate('persistedInProfiles')
|
||||
.setValue(
|
||||
behaviorContent.getChild('persistedInProfiles').getStringValue()
|
||||
)
|
||||
.setType('String')
|
||||
.setLabel(_('Save profile names'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Comma-separated list of profile names in which the object is saved. When a save state is created with one or more profile names specified, the object will be saved only if it matches one of these profiles.'
|
||||
)
|
||||
)
|
||||
.setAdvanced(true);
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
|
||||
saveConfigurationBehavior.initializeContent = function (behaviorContent) {
|
||||
behaviorContent
|
||||
.addChild('defaultProfilePersistence')
|
||||
.setStringValue('Persisted');
|
||||
behaviorContent.addChild('persistedInProfiles').setStringValue('');
|
||||
};
|
||||
|
||||
const sharedData = new gd.BehaviorsSharedData();
|
||||
|
||||
extension
|
||||
.addBehavior(
|
||||
'SaveConfiguration',
|
||||
_('Save state configuration'),
|
||||
'SaveConfiguration',
|
||||
_('Allow the customize how the object is persisted in a save state.'),
|
||||
'',
|
||||
'res/actions/saveUp.svg',
|
||||
'SaveConfiguration',
|
||||
// @ts-ignore - TODO: Fix type being a BehaviorJsImplementation instead of an Behavior
|
||||
saveConfigurationBehavior,
|
||||
sharedData
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
|
||||
);
|
||||
|
||||
return extension;
|
||||
},
|
||||
runExtensionSanityTests: function (gd, extension) {
|
||||
|
@@ -1,32 +0,0 @@
|
||||
namespace gdjs {
|
||||
// const logger = new gdjs.Logger('Save State');
|
||||
|
||||
export class SaveConfigurationRuntimeBehavior extends gdjs.RuntimeBehavior {
|
||||
private readonly _defaultProfilePersistence: 'Persisted' | 'DoNotSave' =
|
||||
'Persisted';
|
||||
private readonly _persistedInProfiles = '';
|
||||
|
||||
constructor(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
behaviorData: any,
|
||||
owner: RuntimeObject
|
||||
) {
|
||||
super(instanceContainer, behaviorData, owner);
|
||||
this._defaultProfilePersistence =
|
||||
behaviorData.defaultProfilePersistence || 'Persisted';
|
||||
this._persistedInProfiles = behaviorData.persistedInProfiles || '';
|
||||
}
|
||||
|
||||
getDefaultProfilePersistence() {
|
||||
return this._defaultProfilePersistence;
|
||||
}
|
||||
|
||||
getPersistedInProfiles() {
|
||||
return this._persistedInProfiles;
|
||||
}
|
||||
}
|
||||
gdjs.registerBehavior(
|
||||
'SaveState::SaveConfiguration',
|
||||
gdjs.SaveConfigurationRuntimeBehavior
|
||||
);
|
||||
}
|
@@ -1,696 +0,0 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Save State');
|
||||
const debugLogger = new gdjs.Logger('Save State - Debug');
|
||||
// Comment this to see message logs and ease debugging:
|
||||
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(
|
||||
'Save State - Debug'
|
||||
);
|
||||
|
||||
type ArbitrarySaveConfiguration = {
|
||||
defaultProfilePersistence: 'Persisted' | 'DoNotSave';
|
||||
persistedInProfiles: Set<string>;
|
||||
};
|
||||
|
||||
export type RestoreRequestOptions = {
|
||||
profileNames: string[];
|
||||
clearSceneStack: boolean;
|
||||
|
||||
fromStorageName?: string;
|
||||
fromVariable?: gdjs.Variable;
|
||||
};
|
||||
|
||||
export namespace saveState {
|
||||
export const getIndexedDbDatabaseName = () => {
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
return `gdevelop-game-${gameId}`;
|
||||
};
|
||||
export const getIndexedDbObjectStore = () => {
|
||||
return `game-saves`;
|
||||
};
|
||||
export const getIndexedDbStorageKey = (key: string) => {
|
||||
return `save-${key}`;
|
||||
};
|
||||
|
||||
const variablesSaveConfiguration: WeakMap<
|
||||
Variable,
|
||||
ArbitrarySaveConfiguration
|
||||
> = new WeakMap();
|
||||
const runtimeSceneDataSaveConfiguration: WeakMap<
|
||||
RuntimeGame,
|
||||
Record<string, ArbitrarySaveConfiguration>
|
||||
> = new WeakMap();
|
||||
const runtimeGameDataSaveConfiguration: WeakMap<
|
||||
RuntimeGame,
|
||||
ArbitrarySaveConfiguration
|
||||
> = new WeakMap();
|
||||
|
||||
export const setVariableSaveConfiguration = (
|
||||
_: gdjs.RuntimeScene,
|
||||
variable: gdjs.Variable,
|
||||
persistInDefaultProfile: boolean,
|
||||
persistedInProfilesAsString: string
|
||||
) => {
|
||||
variablesSaveConfiguration.set(variable, {
|
||||
defaultProfilePersistence: persistInDefaultProfile
|
||||
? 'Persisted'
|
||||
: 'DoNotSave',
|
||||
persistedInProfiles: new Set(
|
||||
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const setSceneDataSaveConfiguration = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
sceneName: string,
|
||||
persistInDefaultProfile: boolean,
|
||||
persistedInProfilesAsString: string
|
||||
) => {
|
||||
const runtimeSceneDataSaveConfigurations =
|
||||
runtimeSceneDataSaveConfiguration.get(runtimeScene.getGame()) || {};
|
||||
|
||||
runtimeSceneDataSaveConfiguration.set(runtimeScene.getGame(), {
|
||||
...runtimeSceneDataSaveConfigurations,
|
||||
[sceneName]: {
|
||||
defaultProfilePersistence: persistInDefaultProfile
|
||||
? 'Persisted'
|
||||
: 'DoNotSave',
|
||||
persistedInProfiles: new Set(
|
||||
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const setGameDataSaveConfiguration = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
persistInDefaultProfile: boolean,
|
||||
persistedInProfilesAsString: string
|
||||
) => {
|
||||
runtimeGameDataSaveConfiguration.set(runtimeScene.getGame(), {
|
||||
defaultProfilePersistence: persistInDefaultProfile
|
||||
? 'Persisted'
|
||||
: 'DoNotSave',
|
||||
persistedInProfiles: new Set(
|
||||
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const checkIfIsPersistedInProfiles = (
|
||||
profileNames: string[],
|
||||
configuration: ArbitrarySaveConfiguration | null | undefined
|
||||
) => {
|
||||
if (profileNames.includes('default')) {
|
||||
if (
|
||||
!configuration ||
|
||||
configuration.defaultProfilePersistence === 'Persisted'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration) {
|
||||
for (const profileName of profileNames) {
|
||||
if (configuration.persistedInProfiles.has(profileName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const makeIsVariableExcludedFromSaveState =
|
||||
(profileNames: string[]) => (variable: gdjs.Variable) => {
|
||||
const saveConfiguration = variablesSaveConfiguration.get(variable);
|
||||
return !checkIfIsPersistedInProfiles(
|
||||
profileNames,
|
||||
saveConfiguration || null
|
||||
);
|
||||
};
|
||||
|
||||
let lastSaveTime: number | null = null;
|
||||
let lastLoadTime: number | null = null;
|
||||
let saveJustSucceeded: boolean = false;
|
||||
let saveJustFailed: boolean = false;
|
||||
let loadJustSucceeded: boolean = false;
|
||||
let loadJustFailed: boolean = false;
|
||||
|
||||
let restoreRequestOptions: RestoreRequestOptions | null = null;
|
||||
|
||||
export const getSecondsSinceLastSave = (_: RuntimeScene): number => {
|
||||
if (!lastSaveTime) return -1;
|
||||
return Math.floor((Date.now() - lastSaveTime) / 1000);
|
||||
};
|
||||
export const getSecondsSinceLastLoad = (_: RuntimeScene): number => {
|
||||
if (!lastLoadTime) return -1;
|
||||
return Math.floor((Date.now() - lastLoadTime) / 1000);
|
||||
};
|
||||
export const hasSaveJustSucceeded = (_: RuntimeScene) => {
|
||||
return saveJustSucceeded;
|
||||
};
|
||||
export const hasLoadJustSucceeded = (_: RuntimeScene) => {
|
||||
return loadJustSucceeded;
|
||||
};
|
||||
export const hasSaveJustFailed = (_: RuntimeScene) => {
|
||||
return saveJustFailed;
|
||||
};
|
||||
export const hasLoadJustFailed = (_: RuntimeScene) => {
|
||||
return loadJustFailed;
|
||||
};
|
||||
export const markSaveJustSucceeded = (_: RuntimeScene) => {
|
||||
saveJustSucceeded = true;
|
||||
lastSaveTime = Date.now();
|
||||
};
|
||||
export const markLoadJustSucceeded = (_: RuntimeScene) => {
|
||||
loadJustSucceeded = true;
|
||||
lastLoadTime = Date.now();
|
||||
};
|
||||
export const markSaveJustFailed = (_: RuntimeScene) => {
|
||||
saveJustFailed = true;
|
||||
};
|
||||
export const markLoadJustFailed = (_: RuntimeScene) => {
|
||||
loadJustFailed = true;
|
||||
};
|
||||
|
||||
// Ensure that the condition "save/load just succeeded/failed" are valid only for one frame.
|
||||
gdjs.registerRuntimeScenePostEventsCallback(() => {
|
||||
saveJustSucceeded = false;
|
||||
saveJustFailed = false;
|
||||
loadJustSucceeded = false;
|
||||
loadJustFailed = false;
|
||||
});
|
||||
|
||||
gdjs.registerRuntimeScenePostEventsCallback(
|
||||
(runtimeScene: gdjs.RuntimeScene) => {
|
||||
checkAndRestoreGameSaveStateAtEndOfFrame(runtimeScene);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a Save State from the given game.
|
||||
*
|
||||
* Only objects, variables etc... tagged with at least one of the profiles
|
||||
* given in `options.profileNames` will be saved.
|
||||
*/
|
||||
export const createGameSaveState = (
|
||||
runtimeGame: RuntimeGame,
|
||||
options: {
|
||||
profileNames: string[];
|
||||
}
|
||||
) => {
|
||||
const { profileNames } = options;
|
||||
|
||||
const getNetworkSyncOptions: GetNetworkSyncDataOptions = {
|
||||
syncObjectIdentifiers: true,
|
||||
shouldExcludeVariableFromData:
|
||||
makeIsVariableExcludedFromSaveState(profileNames),
|
||||
syncAllBehaviors: true,
|
||||
syncGameVariables: true,
|
||||
syncSceneTimers: true,
|
||||
syncOnceTriggers: true,
|
||||
syncSounds: true,
|
||||
syncTweens: true,
|
||||
syncLayers: true,
|
||||
syncAsyncTasks: true,
|
||||
syncSceneVisualProps: true,
|
||||
syncFullTileMaps: true,
|
||||
};
|
||||
|
||||
const shouldPersistGameData = checkIfIsPersistedInProfiles(
|
||||
options.profileNames,
|
||||
runtimeGameDataSaveConfiguration.get(runtimeGame)
|
||||
);
|
||||
|
||||
const gameSaveState: GameSaveState = {
|
||||
// Always persist some game data, but limit it to just the scene stack
|
||||
// if asked to not persist the game data.
|
||||
gameNetworkSyncData: runtimeGame.getNetworkSyncData({
|
||||
...getNetworkSyncOptions,
|
||||
syncGameVariables: shouldPersistGameData,
|
||||
syncSounds: shouldPersistGameData,
|
||||
}),
|
||||
layoutNetworkSyncDatas: [],
|
||||
};
|
||||
|
||||
const scenes = runtimeGame.getSceneStack().getAllScenes();
|
||||
scenes.forEach((runtimeScene, index) => {
|
||||
gameSaveState.layoutNetworkSyncDatas[index] = {
|
||||
sceneData: {} as LayoutNetworkSyncData,
|
||||
objectDatas: {},
|
||||
};
|
||||
|
||||
// First collect all object sync data, as they may generate unique
|
||||
// identifiers like their networkId.
|
||||
for (const object of runtimeScene.getAdhocListOfAllInstances()) {
|
||||
// By default, an object which has no SaveConfiguration behavior is like
|
||||
// it has the default profile persistence set to "Persisted".
|
||||
let shouldPersist = profileNames.includes('default');
|
||||
|
||||
// @ts-ignore - access to `_behaviors` is an exceptional case for the SaveConfiguration behavior.
|
||||
for (const behavior of object._behaviors) {
|
||||
if (behavior instanceof gdjs.SaveConfigurationRuntimeBehavior) {
|
||||
// This object has a SaveConfiguration behavior. Check if the configuration is set to
|
||||
// persist it in one of the given profiles.
|
||||
if (
|
||||
(profileNames.includes('default') &&
|
||||
behavior.getDefaultProfilePersistence() === 'Persisted') ||
|
||||
profileNames.some((profileName) =>
|
||||
// TODO: avoid do it for every single object instance?
|
||||
behavior
|
||||
.getPersistedInProfiles()
|
||||
.split(',')
|
||||
.map((profileName) => profileName.trim())
|
||||
.includes(profileName)
|
||||
)
|
||||
) {
|
||||
shouldPersist = true;
|
||||
} else {
|
||||
shouldPersist = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPersist) {
|
||||
const objectSyncData = object.getNetworkSyncData(
|
||||
getNetworkSyncOptions
|
||||
);
|
||||
gameSaveState.layoutNetworkSyncDatas[index].objectDatas[object.id] =
|
||||
objectSyncData;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect scene data after the objects:
|
||||
const shouldPersistSceneData = checkIfIsPersistedInProfiles(
|
||||
options.profileNames,
|
||||
(runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[
|
||||
runtimeScene.getName()
|
||||
]
|
||||
);
|
||||
if (shouldPersistSceneData) {
|
||||
const sceneData = runtimeScene.getNetworkSyncData(
|
||||
getNetworkSyncOptions
|
||||
);
|
||||
if (sceneData) {
|
||||
gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneData;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return gameSaveState;
|
||||
};
|
||||
|
||||
export const createGameSaveStateInVariable = async function (
|
||||
runtimeScene: RuntimeScene,
|
||||
variable: gdjs.Variable,
|
||||
commaSeparatedProfileNames: string
|
||||
) {
|
||||
try {
|
||||
const gameSaveState = createGameSaveState(runtimeScene.getGame(), {
|
||||
profileNames: parseCommaSeparatedProfileNamesOrDefault(
|
||||
commaSeparatedProfileNames
|
||||
),
|
||||
});
|
||||
variable.fromJSObject(gameSaveState);
|
||||
markSaveJustSucceeded(runtimeScene);
|
||||
} catch (error) {
|
||||
logger.error('Error saving to variable:', error);
|
||||
markSaveJustFailed(runtimeScene);
|
||||
}
|
||||
};
|
||||
|
||||
export const createGameSaveStateInStorage = async function (
|
||||
runtimeScene: RuntimeScene,
|
||||
storageKey: string,
|
||||
commaSeparatedProfileNames: string
|
||||
) {
|
||||
try {
|
||||
const gameSaveState = createGameSaveState(runtimeScene.getGame(), {
|
||||
profileNames: parseCommaSeparatedProfileNamesOrDefault(
|
||||
commaSeparatedProfileNames
|
||||
),
|
||||
});
|
||||
await gdjs.indexedDb.saveToIndexedDB(
|
||||
getIndexedDbDatabaseName(),
|
||||
getIndexedDbObjectStore(),
|
||||
getIndexedDbStorageKey(storageKey),
|
||||
gameSaveState
|
||||
);
|
||||
markSaveJustSucceeded(runtimeScene);
|
||||
} catch (error) {
|
||||
logger.error('Error saving to IndexedDB:', error);
|
||||
markSaveJustFailed(runtimeScene);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAndRestoreGameSaveStateAtEndOfFrame = function (
|
||||
runtimeScene: RuntimeScene
|
||||
) {
|
||||
const runtimeGame = runtimeScene.getGame();
|
||||
|
||||
if (!restoreRequestOptions) return;
|
||||
const { fromVariable, fromStorageName, profileNames, clearSceneStack } =
|
||||
restoreRequestOptions;
|
||||
|
||||
// Reset it so we don't load it twice.
|
||||
restoreRequestOptions = null;
|
||||
|
||||
if (fromVariable) {
|
||||
const saveState = fromVariable.toJSObject();
|
||||
|
||||
try {
|
||||
restoreGameSaveState(runtimeGame, saveState, {
|
||||
profileNames,
|
||||
clearSceneStack,
|
||||
});
|
||||
markLoadJustSucceeded(runtimeScene);
|
||||
} catch (error) {
|
||||
logger.error('Error loading from variable:', error);
|
||||
markLoadJustFailed(runtimeScene);
|
||||
}
|
||||
} else if (fromStorageName) {
|
||||
gdjs.indexedDb
|
||||
.loadFromIndexedDB(
|
||||
getIndexedDbDatabaseName(),
|
||||
getIndexedDbObjectStore(),
|
||||
getIndexedDbStorageKey(fromStorageName)
|
||||
)
|
||||
.then((jsonData) => {
|
||||
const saveState = jsonData as GameSaveState;
|
||||
restoreGameSaveState(runtimeGame, saveState, {
|
||||
profileNames,
|
||||
clearSceneStack,
|
||||
});
|
||||
markLoadJustSucceeded(runtimeScene);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error loading from IndexedDB:', error);
|
||||
markLoadJustFailed(runtimeScene);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getInstanceFromNetworkId = ({
|
||||
runtimeScene,
|
||||
objectName,
|
||||
networkId,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
objectName: string;
|
||||
networkId: string;
|
||||
}): gdjs.RuntimeObject | null => {
|
||||
const instances = runtimeScene.getInstancesOf(objectName);
|
||||
if (!instances) {
|
||||
// object does not exist in the scene, cannot find the instance.
|
||||
return null;
|
||||
}
|
||||
let instance =
|
||||
instances.find((instance) => instance.networkId === networkId) || null;
|
||||
|
||||
// Check if there is already an instance with the given network ID.
|
||||
if (instance) {
|
||||
debugLogger.info(
|
||||
`Found instance ${networkId}, will use it for restoring.`
|
||||
);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Instance not found - it must have been deleted. Create it now.
|
||||
debugLogger.info(
|
||||
`Instance ${networkId} not found, creating instance ${objectName}.`
|
||||
);
|
||||
const newInstance = runtimeScene.createObject(objectName);
|
||||
if (!newInstance) {
|
||||
// Object does not exist in the scene, cannot create the instance.
|
||||
return null;
|
||||
}
|
||||
|
||||
newInstance.networkId = networkId;
|
||||
return newInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Restore the game using the given Save State.
|
||||
*
|
||||
* `options.profileNames` is the list of profiles to restore: only objects, variables etc... tagged with at least
|
||||
* one of these profiles will be restored (or recreated if they don't exist, or deleted if not in the save state).
|
||||
* Others will be left untouched.
|
||||
*
|
||||
* If `options.clearSceneStack` is true, all the scenes will be unloaded and re-created
|
||||
* (meaning all instances will be re-created, variables will go back to their initial values, etc...).
|
||||
* Otherwise, the existing scenes will be updated (or unloaded or created if the save state has different scenes).
|
||||
*/
|
||||
export const restoreGameSaveState = (
|
||||
runtimeGame: RuntimeGame,
|
||||
saveState: GameSaveState,
|
||||
options: {
|
||||
profileNames: string[];
|
||||
clearSceneStack: boolean;
|
||||
}
|
||||
): void => {
|
||||
const getObjectNamesToRestoreForRuntimeScene = (
|
||||
runtimeScene: RuntimeScene
|
||||
): Set<string> => {
|
||||
const allObjectData = [];
|
||||
runtimeScene._objects.values(allObjectData);
|
||||
return getObjectNamesIncludedInProfiles(
|
||||
allObjectData,
|
||||
options.profileNames
|
||||
);
|
||||
};
|
||||
|
||||
const updateFromNetworkSyncDataOptions: UpdateFromNetworkSyncDataOptions =
|
||||
{
|
||||
clearSceneStack:
|
||||
options.clearSceneStack === undefined
|
||||
? true
|
||||
: options.clearSceneStack,
|
||||
getExcludedObjectNames: getObjectNamesToRestoreForRuntimeScene,
|
||||
preventSoundsStoppingOnStartup: true,
|
||||
clearInputs: true,
|
||||
keepControl: true,
|
||||
ignoreVariableOwnership: true,
|
||||
shouldExcludeVariableFromUpdate: makeIsVariableExcludedFromSaveState(
|
||||
options.profileNames
|
||||
),
|
||||
};
|
||||
|
||||
// First update the game, which will update the variables,
|
||||
// and set the scene stack to update when ready.
|
||||
if (saveState.gameNetworkSyncData) {
|
||||
const shouldRestoreGameData = checkIfIsPersistedInProfiles(
|
||||
options.profileNames,
|
||||
runtimeGameDataSaveConfiguration.get(runtimeGame)
|
||||
);
|
||||
|
||||
runtimeGame.updateFromNetworkSyncData(
|
||||
shouldRestoreGameData
|
||||
? saveState.gameNetworkSyncData
|
||||
: {
|
||||
// Disable game data restoration if asked to, but
|
||||
// still always keep `ss` (scene stack) restoration as it's always needed.
|
||||
ss: saveState.gameNetworkSyncData.ss,
|
||||
},
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the scene stack updates, as we are at the end of a frame,
|
||||
// we can safely do it.
|
||||
const sceneStack = runtimeGame.getSceneStack();
|
||||
sceneStack.applyUpdateFromNetworkSyncDataIfAny(
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
|
||||
// Then get all scenes, which we assume will be the expected ones
|
||||
// after the load has been done, so we can update them,
|
||||
// and create their objects.
|
||||
const runtimeScenes = sceneStack.getAllScenes();
|
||||
runtimeScenes.forEach((runtimeScene, index) => {
|
||||
const layoutSyncData = saveState.layoutNetworkSyncDatas[index];
|
||||
if (!layoutSyncData) return;
|
||||
|
||||
// List names of objects that must be restored
|
||||
// (and only them - instances of others will be left alone).
|
||||
const objectNamesToRestore =
|
||||
getObjectNamesToRestoreForRuntimeScene(runtimeScene);
|
||||
|
||||
// Create objects first, so they are available for the scene update,
|
||||
// especially so that they have a networkId defined.
|
||||
const allLoadedNetworkIds = new Set<string>();
|
||||
const objectDatas = layoutSyncData.objectDatas;
|
||||
for (const id in objectDatas) {
|
||||
const objectNetworkSyncData = objectDatas[id];
|
||||
const objectName = objectNetworkSyncData.n;
|
||||
if (!objectName) {
|
||||
logger.warn('Tried to recreate an object without a name.');
|
||||
continue;
|
||||
}
|
||||
if (!objectNamesToRestore.has(objectName)) {
|
||||
// Object is in the save state, but not in the profiles to restore, don't restore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Object is both in the save state and in the profiles to restore, restore it.
|
||||
// Either find the existing instance with the same networkId, or create a new one.
|
||||
const networkId = objectNetworkSyncData.networkId || '';
|
||||
allLoadedNetworkIds.add(networkId);
|
||||
|
||||
const object = getInstanceFromNetworkId({
|
||||
runtimeScene,
|
||||
objectName: objectName,
|
||||
networkId,
|
||||
});
|
||||
if (object) {
|
||||
object.updateFromNetworkSyncData(
|
||||
objectNetworkSyncData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean instances of objects that are not in the profiles to restore but not in the save state
|
||||
// (i.e: those who don't have a networkId, or it's not in the save state: they must not exist).
|
||||
for (const objectName of objectNamesToRestore) {
|
||||
// /!\ Clone the instances to avoid it being modified while iterating through them.
|
||||
const objects = [...runtimeScene.getInstancesOf(objectName)];
|
||||
for (const object of objects) {
|
||||
// This is an object instance that is part of the object that are being restored,
|
||||
// but it has not network id (created after the save state was created) or the network
|
||||
// id is not in the save state: it's not part of the save state and must be deleted.
|
||||
if (
|
||||
!object.networkId ||
|
||||
!allLoadedNetworkIds.has(object.networkId)
|
||||
) {
|
||||
object.deleteFromScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rest of the scene last.
|
||||
if (
|
||||
checkIfIsPersistedInProfiles(
|
||||
options.profileNames,
|
||||
(runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[
|
||||
runtimeScene.getName()
|
||||
]
|
||||
)
|
||||
) {
|
||||
runtimeScene.updateFromNetworkSyncData(
|
||||
layoutSyncData.sceneData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parseCommaSeparatedProfileNames = (
|
||||
commaSeparatedProfileNames: string
|
||||
): string[] | null => {
|
||||
if (!commaSeparatedProfileNames) return null;
|
||||
|
||||
return commaSeparatedProfileNames
|
||||
.split(',')
|
||||
.map((profileName) => profileName.trim());
|
||||
};
|
||||
|
||||
const parseCommaSeparatedProfileNamesOrDefault = (
|
||||
commaSeparatedProfileNames: string
|
||||
): string[] => {
|
||||
return (
|
||||
parseCommaSeparatedProfileNames(commaSeparatedProfileNames) || [
|
||||
'default',
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
export const restoreGameSaveStateFromVariable = async function (
|
||||
_: gdjs.RuntimeScene,
|
||||
variable: gdjs.Variable,
|
||||
commaSeparatedProfileNames: string,
|
||||
clearSceneStack: boolean
|
||||
) {
|
||||
// The information is saved, so that the restore can be done
|
||||
// at the end of the frame,
|
||||
// and avoid possible conflicts with running events.
|
||||
restoreRequestOptions = {
|
||||
fromVariable: variable,
|
||||
profileNames: parseCommaSeparatedProfileNamesOrDefault(
|
||||
commaSeparatedProfileNames
|
||||
),
|
||||
clearSceneStack,
|
||||
};
|
||||
};
|
||||
|
||||
export const restoreGameSaveStateFromStorage = async function (
|
||||
_: gdjs.RuntimeScene,
|
||||
storageName: string,
|
||||
commaSeparatedProfileNames: string,
|
||||
clearSceneStack: boolean
|
||||
) {
|
||||
// The information is saved, so that the restore can be done
|
||||
// at the end of the frame,
|
||||
// and avoid possible conflicts with running events.
|
||||
restoreRequestOptions = {
|
||||
fromStorageName: storageName,
|
||||
profileNames: parseCommaSeparatedProfileNamesOrDefault(
|
||||
commaSeparatedProfileNames
|
||||
),
|
||||
clearSceneStack,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute, by looking at the "static" object data (i.e: in the Project Data),
|
||||
* the name of objects which must be restored, based on the given profiles.
|
||||
*/
|
||||
const getObjectNamesIncludedInProfiles = (
|
||||
allObjectData: ObjectData[],
|
||||
profileNames: string[]
|
||||
): Set<string> => {
|
||||
const objectNames = new Set<string>();
|
||||
for (const objectData of allObjectData) {
|
||||
// By default, an object which has no SaveConfiguration behavior is like
|
||||
// it has the default profile persistence set to "Persisted".
|
||||
let includedInProfiles = profileNames.includes('default');
|
||||
|
||||
for (const behaviorData of objectData.behaviors) {
|
||||
if (behaviorData.type !== 'SaveState::SaveConfiguration') continue;
|
||||
|
||||
const defaultProfilePersistence =
|
||||
behaviorData.defaultProfilePersistence === 'Persisted'
|
||||
? 'Persisted'
|
||||
: 'DoNotSave';
|
||||
const persistedInProfiles =
|
||||
typeof behaviorData.persistedInProfiles === 'string'
|
||||
? behaviorData.persistedInProfiles
|
||||
.split(',')
|
||||
.map((profileName: string) => profileName.trim())
|
||||
: [];
|
||||
|
||||
// This object has a SaveConfiguration behavior. Check if the configuration is set to
|
||||
// persist it in one of the given profiles.
|
||||
includedInProfiles = false;
|
||||
|
||||
if (
|
||||
(profileNames.includes('default') &&
|
||||
defaultProfilePersistence === 'Persisted') ||
|
||||
profileNames.some((profileName) =>
|
||||
persistedInProfiles.includes(profileName)
|
||||
)
|
||||
) {
|
||||
// This object must be persisted in one of the given profile.
|
||||
includedInProfiles = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (includedInProfiles) {
|
||||
objectNames.add(objectData.name);
|
||||
}
|
||||
}
|
||||
|
||||
return objectNames;
|
||||
};
|
||||
}
|
||||
}
|
360
Extensions/SaveState/savestatetools.ts
Normal file
360
Extensions/SaveState/savestatetools.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Save state');
|
||||
export type LoadRequestOptions = {
|
||||
loadStorageName?: string;
|
||||
loadVariable?: gdjs.Variable;
|
||||
};
|
||||
|
||||
export namespace saveState {
|
||||
export const getIndexedDbDatabaseName = () => {
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
return `gdevelop-game-${gameId}`;
|
||||
};
|
||||
export const getIndexedDbObjectStore = () => {
|
||||
return `game-saves`;
|
||||
};
|
||||
export const getIndexedDbStorageKey = (key: string) => {
|
||||
return `save-${key}`;
|
||||
};
|
||||
|
||||
const getNetworkSyncOptions: GetNetworkSyncDataOptions = {
|
||||
syncObjectIdentifiers: true,
|
||||
syncAllVariables: true,
|
||||
syncAllBehaviors: true,
|
||||
syncSceneTimers: true,
|
||||
syncOnceTriggers: true,
|
||||
syncSounds: true,
|
||||
syncTweens: true,
|
||||
syncLayers: true,
|
||||
syncAsyncTasks: true,
|
||||
syncSceneVisualProps: true,
|
||||
syncFullTileMaps: true,
|
||||
};
|
||||
const updateFromNetworkSyncDataOptions: UpdateFromNetworkSyncDataOptions = {
|
||||
clearSceneStack: true,
|
||||
preventInitialInstancesCreation: true,
|
||||
preventSoundsStoppingOnStartup: true,
|
||||
clearInputs: true,
|
||||
keepControl: true,
|
||||
ignoreVariableOwnership: true,
|
||||
};
|
||||
|
||||
let lastSaveTime: number | null = null;
|
||||
let lastLoadTime: number | null = null;
|
||||
let saveJustSucceeded: boolean = false;
|
||||
let saveJustFailed: boolean = false;
|
||||
let loadJustSucceeded: boolean = false;
|
||||
let loadJustFailed: boolean = false;
|
||||
|
||||
let loadRequestOptions: LoadRequestOptions | null = null;
|
||||
|
||||
export const getSecondsSinceLastSave = (): number => {
|
||||
if (!lastSaveTime) return -1;
|
||||
return Math.floor((Date.now() - lastSaveTime) / 1000);
|
||||
};
|
||||
export const getSecondsSinceLastLoad = (): number => {
|
||||
if (!lastLoadTime) return -1;
|
||||
return Math.floor((Date.now() - lastLoadTime) / 1000);
|
||||
};
|
||||
export const hasSaveJustSucceeded = () => {
|
||||
return saveJustSucceeded;
|
||||
};
|
||||
export const hasLoadJustSucceeded = () => {
|
||||
return loadJustSucceeded;
|
||||
};
|
||||
export const hasSaveJustFailed = () => {
|
||||
return saveJustFailed;
|
||||
};
|
||||
export const hasLoadJustFailed = () => {
|
||||
return loadJustFailed;
|
||||
};
|
||||
export const markSaveJustSucceeded = () => {
|
||||
saveJustSucceeded = true;
|
||||
lastSaveTime = Date.now();
|
||||
};
|
||||
export const markLoadJustSucceeded = () => {
|
||||
loadJustSucceeded = true;
|
||||
lastLoadTime = Date.now();
|
||||
};
|
||||
export const markSaveJustFailed = () => {
|
||||
saveJustFailed = true;
|
||||
};
|
||||
export const markLoadJustFailed = () => {
|
||||
loadJustFailed = true;
|
||||
};
|
||||
|
||||
// Ensure that the condition "save/load just succeeded/failed" are valid only for one frame.
|
||||
gdjs.registerRuntimeScenePostEventsCallback(() => {
|
||||
saveJustSucceeded = false;
|
||||
saveJustFailed = false;
|
||||
loadJustSucceeded = false;
|
||||
loadJustFailed = false;
|
||||
});
|
||||
|
||||
gdjs.registerRuntimeScenePostEventsCallback(
|
||||
(runtimeScene: gdjs.RuntimeScene) => {
|
||||
loadGameSnapshotAtTheEndOfFrameIfAny(runtimeScene);
|
||||
}
|
||||
);
|
||||
|
||||
const getGameSaveState = (runtimeScene: RuntimeScene) => {
|
||||
const gameSaveState: GameSaveState = {
|
||||
gameNetworkSyncData: {},
|
||||
layoutNetworkSyncDatas: [],
|
||||
};
|
||||
|
||||
const gameData = runtimeScene
|
||||
.getGame()
|
||||
.getNetworkSyncData(getNetworkSyncOptions);
|
||||
const scenes = runtimeScene.getGame().getSceneStack().getAllScenes();
|
||||
gameSaveState.gameNetworkSyncData = gameData || {};
|
||||
|
||||
scenes.forEach((scene, index) => {
|
||||
gameSaveState.layoutNetworkSyncDatas[index] = {
|
||||
sceneData: {} as LayoutNetworkSyncData,
|
||||
objectDatas: {},
|
||||
};
|
||||
|
||||
// First collect all object sync data, as they may generate unique
|
||||
// identifiers like their networkId.
|
||||
const sceneRuntimeObjects = scene.getAdhocListOfAllInstances();
|
||||
for (const key in sceneRuntimeObjects) {
|
||||
if (sceneRuntimeObjects.hasOwnProperty(key)) {
|
||||
const object = sceneRuntimeObjects[key];
|
||||
const objectSyncData = object.getNetworkSyncData(
|
||||
getNetworkSyncOptions
|
||||
);
|
||||
gameSaveState.layoutNetworkSyncDatas[index].objectDatas[object.id] =
|
||||
objectSyncData;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all scene data in the end.
|
||||
const sceneDatas = (scene.getNetworkSyncData(getNetworkSyncOptions) ||
|
||||
[]) as LayoutNetworkSyncData;
|
||||
|
||||
gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneDatas;
|
||||
});
|
||||
|
||||
return gameSaveState;
|
||||
};
|
||||
|
||||
export const saveVariableGameSnapshot = async function (
|
||||
currentScene: RuntimeScene,
|
||||
variable: gdjs.Variable
|
||||
) {
|
||||
try {
|
||||
const gameSaveState = getGameSaveState(currentScene);
|
||||
variable.fromJSObject(gameSaveState);
|
||||
markSaveJustSucceeded();
|
||||
} catch (error) {
|
||||
logger.error('Error saving to variable:', error);
|
||||
markSaveJustFailed();
|
||||
}
|
||||
};
|
||||
|
||||
export const saveStorageGameSnapshot = async function (
|
||||
currentScene: RuntimeScene,
|
||||
storageKey: string
|
||||
) {
|
||||
try {
|
||||
const gameSaveState = getGameSaveState(currentScene);
|
||||
await gdjs.indexedDb.saveToIndexedDB(
|
||||
getIndexedDbDatabaseName(),
|
||||
getIndexedDbObjectStore(),
|
||||
getIndexedDbStorageKey(storageKey),
|
||||
gameSaveState
|
||||
);
|
||||
markSaveJustSucceeded();
|
||||
} catch (error) {
|
||||
logger.error('Error saving to IndexedDB:', error);
|
||||
markSaveJustFailed();
|
||||
}
|
||||
};
|
||||
|
||||
export const loadGameFromVariableSnapshot = async function (
|
||||
variable: gdjs.Variable
|
||||
) {
|
||||
// The information is saved, so that the load can be done
|
||||
// at the end of the frame,
|
||||
// and avoid possible conflicts with running events.
|
||||
loadRequestOptions = {
|
||||
loadVariable: variable,
|
||||
};
|
||||
};
|
||||
|
||||
export const loadGameFromStorageSnapshot = async function (
|
||||
storageName: string
|
||||
) {
|
||||
// The information is saved, so that the load can be done
|
||||
// at the end of the frame,
|
||||
// and avoid possible conflicts with running events.
|
||||
loadRequestOptions = {
|
||||
loadStorageName: storageName,
|
||||
};
|
||||
};
|
||||
|
||||
const loadGameSnapshotAtTheEndOfFrameIfAny = function (
|
||||
runtimeScene: RuntimeScene
|
||||
) {
|
||||
if (!loadRequestOptions) return;
|
||||
|
||||
const optionsToApply = loadRequestOptions;
|
||||
// Reset it so we don't load it twice.
|
||||
loadRequestOptions = null;
|
||||
|
||||
if (optionsToApply.loadVariable) {
|
||||
const sceneVariables = runtimeScene.getVariables();
|
||||
const variablePathInScene =
|
||||
sceneVariables.getVariablePathInContainerByLoopingThroughAllVariables(
|
||||
optionsToApply.loadVariable
|
||||
);
|
||||
const gameVariables = runtimeScene.getGame().getVariables();
|
||||
const variablePathIngame =
|
||||
gameVariables.getVariablePathInContainerByLoopingThroughAllVariables(
|
||||
optionsToApply.loadVariable
|
||||
);
|
||||
const saveState =
|
||||
optionsToApply.loadVariable.toJSObject() as GameSaveState;
|
||||
|
||||
try {
|
||||
loadGameFromSave(runtimeScene, saveState, {
|
||||
variableToRehydrate: optionsToApply.loadVariable,
|
||||
variablePathInScene: variablePathInScene,
|
||||
variablePathInGame: variablePathIngame,
|
||||
});
|
||||
markLoadJustSucceeded();
|
||||
} catch (error) {
|
||||
logger.error('Error loading from variable:', error);
|
||||
markLoadJustFailed();
|
||||
}
|
||||
} else if (optionsToApply.loadStorageName) {
|
||||
gdjs.indexedDb
|
||||
.loadFromIndexedDB(
|
||||
getIndexedDbDatabaseName(),
|
||||
getIndexedDbObjectStore(),
|
||||
getIndexedDbStorageKey(optionsToApply.loadStorageName)
|
||||
)
|
||||
.then((jsonData) => {
|
||||
const saveState = jsonData as GameSaveState;
|
||||
loadGameFromSave(runtimeScene, saveState);
|
||||
markLoadJustSucceeded();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error loading from IndexedDB:', error);
|
||||
markLoadJustFailed();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadGameFromSave = (
|
||||
runtimeScene: RuntimeScene,
|
||||
saveState: GameSaveState,
|
||||
saveOptions?: {
|
||||
variableToRehydrate: gdjs.Variable;
|
||||
variablePathInScene: string[] | null;
|
||||
variablePathInGame: string[] | null;
|
||||
}
|
||||
): void => {
|
||||
// Save the content of the save, as it will be erased after the load.
|
||||
const variableToRehydrateNetworkSyncData = saveOptions
|
||||
? saveOptions.variableToRehydrate.getNetworkSyncData(
|
||||
getNetworkSyncOptions
|
||||
)
|
||||
: null;
|
||||
|
||||
// First update the game, which will update the variables,
|
||||
// and set the scene stack to update when ready.
|
||||
const runtimeGame = runtimeScene.getGame();
|
||||
runtimeGame.updateFromNetworkSyncData(
|
||||
saveState.gameNetworkSyncData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
|
||||
// Apply the scene stack updates, as we are at the end of a frame,
|
||||
// we can safely do it.
|
||||
const sceneStack = runtimeGame.getSceneStack();
|
||||
sceneStack.applyUpdateFromNetworkSyncDataIfAny(
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
|
||||
// Then get all scenes, which we assume will be the expected ones
|
||||
// after the load has been done, so we can update them,
|
||||
// and create their objects.
|
||||
const runtimeScenes = sceneStack.getAllScenes();
|
||||
runtimeScenes.forEach((scene, index) => {
|
||||
const layoutSyncData = saveState.layoutNetworkSyncDatas[index];
|
||||
if (!layoutSyncData) return;
|
||||
|
||||
// Create objects first, so they are available for the scene update,
|
||||
// especially so that they have a networkId defined.
|
||||
const objectDatas = layoutSyncData.objectDatas;
|
||||
for (const id in objectDatas) {
|
||||
const objectNetworkSyncData = objectDatas[id];
|
||||
const objectName = objectNetworkSyncData.n;
|
||||
if (!objectName) {
|
||||
logger.warn('Tried to recreate an object without a name.');
|
||||
continue;
|
||||
}
|
||||
const object = scene.createObject(objectName);
|
||||
if (object) {
|
||||
object.updateFromNetworkSyncData(
|
||||
objectNetworkSyncData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the scene last.
|
||||
scene.updateFromNetworkSyncData(
|
||||
layoutSyncData.sceneData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
});
|
||||
|
||||
// Finally, if the save was done in a variable,
|
||||
// rehydrate the variable where the save was done,
|
||||
// as it has been erased by the load.
|
||||
if (saveOptions && variableToRehydrateNetworkSyncData) {
|
||||
const currentScene = sceneStack.getCurrentScene();
|
||||
if (!currentScene) return;
|
||||
const sceneVariables = currentScene.getVariables();
|
||||
const gameVariables = currentScene.getGame().getVariables();
|
||||
const { variablePathInScene, variablePathInGame } = saveOptions;
|
||||
|
||||
if (variablePathInScene && variablePathInScene.length > 0) {
|
||||
const variableName =
|
||||
variablePathInScene[variablePathInScene.length - 1];
|
||||
const variableInScene =
|
||||
sceneVariables.getVariableFromPath(variablePathInScene);
|
||||
if (variableInScene) {
|
||||
const variableNetworkSyncData: VariableNetworkSyncData = {
|
||||
name: variableName,
|
||||
...variableToRehydrateNetworkSyncData,
|
||||
};
|
||||
variableInScene.updateFromNetworkSyncData(
|
||||
variableNetworkSyncData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
if (variablePathInGame && variablePathInGame.length > 0) {
|
||||
const variableName =
|
||||
variablePathInGame[variablePathInGame.length - 1];
|
||||
const variableInGame =
|
||||
gameVariables.getVariableFromPath(variablePathInGame);
|
||||
if (variableInGame) {
|
||||
const variableNetworkSyncData: VariableNetworkSyncData = {
|
||||
name: variableName,
|
||||
...variableToRehydrateNetworkSyncData,
|
||||
};
|
||||
variableInGame.updateFromNetworkSyncData(
|
||||
variableNetworkSyncData,
|
||||
updateFromNetworkSyncDataOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -203,13 +203,17 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
|
||||
const loadedSpineAtlas = this._loadedSpineAtlases.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (loadedSpineAtlas) {
|
||||
loadedSpineAtlas.dispose();
|
||||
this._loadedSpineAtlases.delete(resourceData);
|
||||
}
|
||||
|
||||
const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
|
||||
const loadingSpineAtlas = this._loadingSpineAtlases.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (loadingSpineAtlas) {
|
||||
loadingSpineAtlas.then((atl) => atl.dispose());
|
||||
this._loadingSpineAtlases.delete(resourceData);
|
||||
|
@@ -218,15 +218,13 @@ namespace gdjs {
|
||||
this.setSize(initialInstanceData.width, initialInstanceData.height);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
}
|
||||
|
||||
getDrawableX(): number {
|
||||
|
@@ -61,6 +61,19 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
|
||||
instances: [],
|
||||
variables: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -141,7 +141,9 @@ namespace gdjs {
|
||||
);
|
||||
this._borderOpacity = objectData.content.borderOpacity;
|
||||
this._borderWidth = objectData.content.borderWidth;
|
||||
this._disabled = objectData.content.disabled;
|
||||
this._disabled = instanceContainer.getGame().isInGameEdition()
|
||||
? true
|
||||
: objectData.content.disabled;
|
||||
this._readOnly = objectData.content.readOnly;
|
||||
this._spellCheck =
|
||||
objectData.content.spellCheck !== undefined
|
||||
@@ -334,9 +336,11 @@ namespace gdjs {
|
||||
this.setHeight(initialInstanceData.height);
|
||||
this._renderer.updatePadding();
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
onScenePaused(runtimeScene: gdjs.RuntimeScene): void {
|
||||
@@ -566,6 +570,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setDisabled(value: boolean) {
|
||||
if (this.getInstanceContainer().getGame().isInGameEdition()) {
|
||||
return;
|
||||
}
|
||||
this._disabled = value;
|
||||
this._renderer.updateDisabled();
|
||||
}
|
||||
|
@@ -340,7 +340,9 @@ namespace gdjs {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
override update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
override updatePreRender(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
@@ -358,9 +360,11 @@ namespace gdjs {
|
||||
} else {
|
||||
this.setWrapping(false);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -236,9 +236,11 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
|
||||
// 4. Update position (calculations based on renderer's dimensions).
|
||||
this._renderer.updatePosition();
|
||||
@@ -449,6 +451,14 @@ namespace gdjs {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this.getTileMapWidth();
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this.getTileMapHeight();
|
||||
}
|
||||
|
||||
getScaleX(): float {
|
||||
return this._renderer.getScaleX();
|
||||
}
|
||||
|
@@ -57,6 +57,19 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -197,9 +197,11 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
private _updateTileMap(): void {
|
||||
@@ -343,6 +345,14 @@ namespace gdjs {
|
||||
this.setHeight(newHeight);
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this.getTileMapWidth();
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this.getTileMapHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
|
@@ -42,6 +42,8 @@ namespace gdjs {
|
||||
|
||||
_renderer: gdjs.TiledSpriteRuntimeObjectRenderer;
|
||||
|
||||
_objectData: TiledSpriteObjectData;
|
||||
|
||||
/**
|
||||
* @param instanceContainer The container the object belongs to.
|
||||
* @param tiledSpriteObjectData The initial properties of the object
|
||||
@@ -51,6 +53,7 @@ namespace gdjs {
|
||||
tiledSpriteObjectData: TiledSpriteObjectData
|
||||
) {
|
||||
super(instanceContainer, tiledSpriteObjectData);
|
||||
this._objectData = tiledSpriteObjectData;
|
||||
this._renderer = new gdjs.TiledSpriteRuntimeObjectRenderer(
|
||||
this,
|
||||
instanceContainer,
|
||||
@@ -66,6 +69,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
updateFromObjectData(oldObjectData, newObjectData): boolean {
|
||||
this._objectData = newObjectData;
|
||||
if (oldObjectData.texture !== newObjectData.texture) {
|
||||
this.setTexture(newObjectData.texture, this.getRuntimeScene());
|
||||
}
|
||||
@@ -129,9 +133,11 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,6 +229,14 @@ namespace gdjs {
|
||||
this.setHeight(height);
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this._objectData.width;
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this._objectData.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the offset on the X-axis when displaying the image of the Tiled Sprite object.
|
||||
* @param xOffset The new offset on the X-axis.
|
||||
|
@@ -34,6 +34,19 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
|
||||
objects: [],
|
||||
instances: [],
|
||||
usedResources: [],
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridDepth: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridOffsetZ: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
usedExtensionsWithVariablesData: [],
|
||||
});
|
||||
|
@@ -151,9 +151,11 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
}
|
||||
|
||||
onDestroyed(): void {
|
||||
|
@@ -18,6 +18,9 @@
|
||||
#include "GDCore/IDE/Project/ProjectResourcesCopier.h"
|
||||
#include "GDCore/IDE/Project/SceneResourcesFinder.h"
|
||||
#include "GDCore/IDE/ProjectStripper.h"
|
||||
#include "GDCore/Project/EventsBasedObject.h"
|
||||
#include "GDCore/Project/EventsBasedObjectVariant.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtension.h"
|
||||
#include "GDCore/Project/ExternalEvents.h"
|
||||
#include "GDCore/Project/ExternalLayout.h"
|
||||
#include "GDCore/Project/Layout.h"
|
||||
@@ -47,7 +50,7 @@ Exporter::~Exporter() {}
|
||||
bool Exporter::ExportProjectForPixiPreview(
|
||||
const PreviewExportOptions &options) {
|
||||
ExporterHelper helper(fs, gdjsRoot, codeOutputDir);
|
||||
return helper.ExportProjectForPixiPreview(options);
|
||||
return helper.ExportProjectForPixiPreview(options, includesFiles);
|
||||
}
|
||||
|
||||
bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
@@ -80,7 +83,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
|
||||
// Prepare the export directory
|
||||
fs.MkDir(exportDir);
|
||||
std::vector<gd::String> includesFiles;
|
||||
includesFiles.clear();
|
||||
std::vector<gd::String> resourcesFiles;
|
||||
|
||||
// Export the resources (before generating events as some resources
|
||||
@@ -98,6 +101,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
helper.AddLibsInclude(
|
||||
/*pixiRenderers=*/true,
|
||||
usedExtensionsResult.Has3DObjects(),
|
||||
/*isInGameEditor=*/false,
|
||||
/*includeWebsocketDebuggerClient=*/false,
|
||||
/*includeWindowMessageDebuggerClient=*/false,
|
||||
/*includeMinimalDebuggerClient=*/false,
|
||||
@@ -120,7 +124,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
helper.ExportEffectIncludes(exportedProject, includesFiles);
|
||||
|
||||
// Export events
|
||||
if (!helper.ExportEventsCode(exportedProject,
|
||||
if (!helper.ExportScenesEventsCode(exportedProject,
|
||||
codeOutputDir,
|
||||
includesFiles,
|
||||
wholeProjectDiagnosticReport,
|
||||
@@ -130,29 +134,11 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 like objects groups...)...
|
||||
gd::ProjectStripper::StripProjectForExport(exportedProject);
|
||||
|
||||
//...and export it
|
||||
gd::SerializerElement noRuntimeGameOptions;
|
||||
helper.ExportProjectData(fs,
|
||||
exportedProject,
|
||||
codeOutputDir + "/data.js",
|
||||
helper.ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
|
||||
noRuntimeGameOptions,
|
||||
projectUsedResources,
|
||||
scenesUsedResources);
|
||||
/*isInGameEdition=*/false);
|
||||
includesFiles.push_back(codeOutputDir + "/data.js");
|
||||
|
||||
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
|
||||
@@ -215,4 +201,17 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Exporter::SerializeProjectData(const gd::Project &project,
|
||||
const PreviewExportOptions &options,
|
||||
gd::SerializerElement &projectDataElement) {
|
||||
ExporterHelper::SerializeProjectData(fs, project, options, projectDataElement);
|
||||
}
|
||||
|
||||
void Exporter::SerializeRuntimeGameOptions(
|
||||
const PreviewExportOptions &options,
|
||||
gd::SerializerElement &runtimeGameOptionsElement) {
|
||||
ExporterHelper::SerializeRuntimeGameOptions(
|
||||
fs, gdjsRoot, options, includesFiles, runtimeGameOptionsElement);
|
||||
}
|
||||
|
||||
} // namespace gdjs
|
||||
|
@@ -16,6 +16,7 @@ class Project;
|
||||
class Layout;
|
||||
class ExternalLayout;
|
||||
class AbstractFileSystem;
|
||||
class SerializerElement;
|
||||
} // namespace gd
|
||||
namespace gdjs {
|
||||
struct PreviewExportOptions;
|
||||
@@ -64,7 +65,33 @@ class Exporter {
|
||||
codeOutputDir = codeOutputDir_;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* \brief Serialize a project without its events to JSON
|
||||
*
|
||||
* \param project The project to be exported
|
||||
* \param options The content of the extra configuration
|
||||
* \param projectDataElement The element where the project data is serialized
|
||||
*/
|
||||
void SerializeProjectData(const gd::Project &project,
|
||||
const PreviewExportOptions &options,
|
||||
gd::SerializerElement &projectDataElement);
|
||||
|
||||
/**
|
||||
* \brief Serialize the content of the extra configuration to store
|
||||
* in gdjs.runtimeGameOptions to JSON
|
||||
*
|
||||
* \warning `ExportProjectForPixiPreview` must be called first to serialize
|
||||
* the list of scripts files.
|
||||
*
|
||||
* \param options The content of the extra configuration
|
||||
* \param runtimeGameOptionsElement The element where the game options are
|
||||
* serialized
|
||||
*/
|
||||
void
|
||||
SerializeRuntimeGameOptions(const PreviewExportOptions &options,
|
||||
gd::SerializerElement &runtimeGameOptionsElement);
|
||||
|
||||
private:
|
||||
gd::AbstractFileSystem&
|
||||
fs; ///< The abstract file system to be used for exportation.
|
||||
gd::String lastError; ///< The last error that occurred.
|
||||
@@ -72,6 +99,8 @@ class Exporter {
|
||||
gdjsRoot; ///< The root directory of GDJS, used to copy runtime files.
|
||||
gd::String codeOutputDir; ///< The directory where JS code is outputted. Will
|
||||
///< be then copied to the final output directory.
|
||||
std::vector<gd::String>
|
||||
includesFiles; ///< The list of scripts files - useful for hot-reloading
|
||||
};
|
||||
|
||||
} // namespace gdjs
|
||||
|
@@ -28,10 +28,13 @@
|
||||
#include "GDCore/IDE/Events/UsedExtensionsFinder.h"
|
||||
#include "GDCore/IDE/ExportedDependencyResolver.h"
|
||||
#include "GDCore/IDE/Project/ProjectResourcesCopier.h"
|
||||
#include "GDCore/IDE/Project/ResourcesMergingHelper.h"
|
||||
#include "GDCore/IDE/Project/SceneResourcesFinder.h"
|
||||
#include "GDCore/IDE/ProjectStripper.h"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
#include "GDCore/IDE/SceneNameMangler.h"
|
||||
#include "GDCore/Project/EventsBasedObject.h"
|
||||
#include "GDCore/Project/EventsBasedObjectVariant.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtension.h"
|
||||
#include "GDCore/Project/ExternalEvents.h"
|
||||
#include "GDCore/Project/ExternalLayout.h"
|
||||
@@ -58,7 +61,6 @@ 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
|
||||
@@ -104,128 +106,298 @@ ExporterHelper::ExporterHelper(gd::AbstractFileSystem &fileSystem,
|
||||
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_) {};
|
||||
|
||||
bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
const PreviewExportOptions &options) {
|
||||
const PreviewExportOptions &options,
|
||||
std::vector<gd::String> &includesFiles) {
|
||||
|
||||
if (options.isInGameEdition && !options.shouldReloadProjectData &&
|
||||
!options.shouldReloadLibraries && !options.shouldGenerateScenesEventsCode) {
|
||||
gd::LogStatus("Skip project export entirely");
|
||||
return "";
|
||||
}
|
||||
|
||||
double previousTime = GetTimeNow();
|
||||
fs.MkDir(options.exportPath);
|
||||
fs.ClearDir(options.exportPath);
|
||||
std::vector<gd::String> includesFiles;
|
||||
if (options.shouldClearExportFolder) {
|
||||
fs.ClearDir(options.exportPath);
|
||||
}
|
||||
includesFiles.clear();
|
||||
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;
|
||||
const gd::Project &immutableProject = options.project;
|
||||
previousTime = LogTimeSpent("Project cloning", previousTime);
|
||||
|
||||
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);
|
||||
if (options.isInGameEdition) {
|
||||
if (options.shouldReloadProjectData || options.shouldGenerateScenesEventsCode) {
|
||||
auto projectDirectory = fs.DirNameFrom(exportedProject.GetProjectFile());
|
||||
gd::ResourcesMergingHelper resourcesMergingHelper(
|
||||
exportedProject.GetResourcesManager(), fs);
|
||||
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
|
||||
resourcesMergingHelper.SetShouldUseOriginalAbsoluteFilenames();
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(exportedProject,
|
||||
resourcesMergingHelper);
|
||||
|
||||
previousTime = LogTimeSpent("Resource path resolving", previousTime);
|
||||
}
|
||||
gd::LogStatus("Resource export is skipped");
|
||||
} 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);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
/*includeInAppTutorialMessage*/
|
||||
!options.inAppTutorialMessageInPreview.empty(),
|
||||
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);
|
||||
if (options.shouldReloadProjectData || options.shouldGenerateScenesEventsCode) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Export effects (after engine libraries as they auto-register themselves to
|
||||
// the engine)
|
||||
ExportEffectIncludes(exportedProject, includesFiles);
|
||||
std::vector<gd::SourceFileMetadata> noUsedSourceFiles;
|
||||
std::vector<gd::SourceFileMetadata> &usedSourceFiles = noUsedSourceFiles;
|
||||
if (options.shouldReloadLibraries) {
|
||||
auto usedExtensionsResult =
|
||||
gd::UsedExtensionsFinder::ScanProject(exportedProject);
|
||||
usedSourceFiles = usedExtensionsResult.GetUsedSourceFiles();
|
||||
|
||||
previousTime = LogTimeSpent("Include files export", previousTime);
|
||||
// Export engine libraries
|
||||
AddLibsInclude(/*pixiRenderers=*/true,
|
||||
/*pixiInThreeRenderers=*/
|
||||
usedExtensionsResult.Has3DObjects(),
|
||||
/*isInGameEdition=*/
|
||||
options.isInGameEdition,
|
||||
/*includeWebsocketDebuggerClient=*/
|
||||
!options.websocketDebuggerServerAddress.empty(),
|
||||
/*includeWindowMessageDebuggerClient=*/
|
||||
options.useWindowMessageDebuggerClient,
|
||||
/*includeMinimalDebuggerClient=*/
|
||||
options.useMinimalDebuggerClient,
|
||||
/*includeCaptureManager=*/
|
||||
!options.captureOptions.IsEmpty(),
|
||||
/*includeInAppTutorialMessage*/
|
||||
!options.inAppTutorialMessageInPreview.empty(),
|
||||
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
|
||||
includesFiles);
|
||||
|
||||
if (!options.projectDataOnlyExport) {
|
||||
// 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);
|
||||
}
|
||||
if (options.isInGameEdition) {
|
||||
// TODO Scan the objects and events of event-based objects
|
||||
// (it could be an alternative method ScanProjectAndEventsBasedObjects in
|
||||
// UsedExtensionsFinder).
|
||||
// This is already done by UsedExtensionsFinder, but maybe it shouldn't.
|
||||
|
||||
// Export all event-based objects because they can be edited even if they
|
||||
// are not used yet.
|
||||
for (std::size_t e = 0;
|
||||
e < exportedProject.GetEventsFunctionsExtensionsCount(); e++) {
|
||||
auto &eventsFunctionsExtension =
|
||||
exportedProject.GetEventsFunctionsExtension(e);
|
||||
|
||||
for (auto &&eventsBasedObjectUniquePtr :
|
||||
eventsFunctionsExtension.GetEventsBasedObjects()
|
||||
.GetInternalVector()) {
|
||||
auto eventsBasedObject = eventsBasedObjectUniquePtr.get();
|
||||
|
||||
auto metadata = gd::MetadataProvider::GetExtensionAndObjectMetadata(
|
||||
exportedProject.GetCurrentPlatform(),
|
||||
gd::PlatformExtension::GetObjectFullType(
|
||||
eventsFunctionsExtension.GetName(),
|
||||
eventsBasedObject->GetName()));
|
||||
for (auto &&includeFile : metadata.GetMetadata().includeFiles) {
|
||||
InsertUnique(includesFiles, includeFile);
|
||||
}
|
||||
for (auto &behaviorType :
|
||||
metadata.GetMetadata().GetDefaultBehaviors()) {
|
||||
auto behaviorMetadata =
|
||||
gd::MetadataProvider::GetExtensionAndBehaviorMetadata(
|
||||
exportedProject.GetCurrentPlatform(), behaviorType);
|
||||
for (auto &&includeFile :
|
||||
behaviorMetadata.GetMetadata().includeFiles) {
|
||||
InsertUnique(includesFiles, includeFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export effects (after engine libraries as they auto-register themselves to
|
||||
// the engine)
|
||||
ExportEffectIncludes(exportedProject, includesFiles);
|
||||
|
||||
previousTime = LogTimeSpent("Include files export", previousTime);
|
||||
}
|
||||
else {
|
||||
gd::LogStatus("Include files export is skipped");
|
||||
}
|
||||
|
||||
if (options.shouldGenerateScenesEventsCode) {
|
||||
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport =
|
||||
options.project.GetWholeProjectDiagnosticReport();
|
||||
wholeProjectDiagnosticReport.Clear();
|
||||
|
||||
// Generate events code
|
||||
if (!ExportEventsCode(immutableProject,
|
||||
if (!ExportScenesEventsCode(immutableProject,
|
||||
codeOutputDir,
|
||||
includesFiles,
|
||||
wholeProjectDiagnosticReport,
|
||||
true)) {
|
||||
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);
|
||||
else {
|
||||
gd::LogStatus("Events code export is skipped");
|
||||
}
|
||||
|
||||
// Strip the project (*after* generating events as the events may use stripped
|
||||
// things (objects groups...))
|
||||
gd::ProjectStripper::StripProjectForExport(exportedProject);
|
||||
exportedProject.SetFirstLayout(options.layoutName);
|
||||
if (options.shouldReloadProjectData) {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
previousTime = LogTimeSpent("Data stripping", previousTime);
|
||||
gd::SerializerElement runtimeGameOptions;
|
||||
ExporterHelper::SerializeRuntimeGameOptions(fs, gdjsRoot, options,
|
||||
includesFiles, runtimeGameOptions);
|
||||
ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
|
||||
runtimeGameOptions, options.isInGameEdition);
|
||||
includesFiles.push_back(codeOutputDir + "/data.js");
|
||||
|
||||
previousTime = LogTimeSpent("Project data export", previousTime);
|
||||
}
|
||||
else {
|
||||
gd::LogStatus("Project data export is skipped");
|
||||
}
|
||||
|
||||
if (options.shouldReloadLibraries) {
|
||||
if (options.isInGameEdition) {
|
||||
InsertUnique(resourcesFiles, "InGameEditor/Resources/primitivedrawingicon.png");
|
||||
}
|
||||
// Copy all the dependencies and their source maps
|
||||
ExportIncludesAndLibs(includesFiles, options.exportPath, true);
|
||||
ExportIncludesAndLibs(resourcesFiles, options.exportPath, true);
|
||||
|
||||
// TODO Build a full includesFiles list without actually doing export or
|
||||
// generation.
|
||||
if (options.shouldGenerateScenesEventsCode) {
|
||||
// Create the index file
|
||||
if (!ExportIndexFile(exportedProject, gdjsRoot + "/Runtime/index.html",
|
||||
options.exportPath, includesFiles, usedSourceFiles,
|
||||
options.nonRuntimeScriptsCacheBurst,
|
||||
"gdjs.runtimeGameOptions")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
previousTime = LogTimeSpent("Include and libs export", previousTime);
|
||||
} else {
|
||||
gd::LogStatus("Include and libs export is skipped");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
gd::String ExporterHelper::ExportProjectData(
|
||||
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
|
||||
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition) {
|
||||
fs.MkDir(fs.DirNameFrom(filename));
|
||||
|
||||
gd::SerializerElement projectDataElement;
|
||||
ExporterHelper::StriptAndSerializeProjectData(
|
||||
project, projectDataElement, isInGameEdition);
|
||||
|
||||
// Save the project to JSON
|
||||
gd::String output =
|
||||
"gdjs.projectData = " + gd::Serializer::ToJSON(projectDataElement) +
|
||||
";\ngdjs.runtimeGameOptions = " + gd::Serializer::ToJSON(runtimeGameOptions) +
|
||||
";\n";
|
||||
|
||||
if (!fs.WriteToFile(filename, output))
|
||||
return "Unable to write " + filename;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void ExporterHelper::SerializeRuntimeGameOptions(
|
||||
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
|
||||
const PreviewExportOptions &options, std::vector<gd::String> &includesFiles,
|
||||
gd::SerializerElement &runtimeGameOptions) {
|
||||
// Create the setup options passed to the gdjs.RuntimeGame
|
||||
gd::SerializerElement runtimeGameOptions;
|
||||
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
runtimeGameOptions.AddChild("injectExternalLayout")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
auto &initialRuntimeGameStatus =
|
||||
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
|
||||
initialRuntimeGameStatus.AddChild("sceneName")
|
||||
.SetStringValue(options.layoutName);
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
|
||||
initialRuntimeGameStatus.AddChild("editorId").SetValue(options.editorId);
|
||||
if (!options.editorCamera3DCameraMode.empty()) {
|
||||
auto &editorCamera3D =
|
||||
initialRuntimeGameStatus.AddChild("editorCamera3D");
|
||||
editorCamera3D.AddChild("cameraMode").SetStringValue(
|
||||
options.editorCamera3DCameraMode);
|
||||
editorCamera3D.AddChild("positionX")
|
||||
.SetDoubleValue(options.editorCamera3DPositionX);
|
||||
editorCamera3D.AddChild("positionY")
|
||||
.SetDoubleValue(options.editorCamera3DPositionY);
|
||||
editorCamera3D.AddChild("positionZ")
|
||||
.SetDoubleValue(options.editorCamera3DPositionZ);
|
||||
editorCamera3D.AddChild("rotationAngle")
|
||||
.SetDoubleValue(options.editorCamera3DRotationAngle);
|
||||
editorCamera3D.AddChild("elevationAngle")
|
||||
.SetDoubleValue(options.editorCamera3DElevationAngle);
|
||||
editorCamera3D.AddChild("distance")
|
||||
.SetDoubleValue(options.editorCamera3DDistance);
|
||||
}
|
||||
}
|
||||
runtimeGameOptions.AddChild("projectDataOnlyExport")
|
||||
.SetBoolValue(options.projectDataOnlyExport);
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
|
||||
.SetBoolValue(true);
|
||||
}
|
||||
}
|
||||
if (!options.eventsBasedObjectType.empty()) {
|
||||
initialRuntimeGameStatus.AddChild("eventsBasedObjectType")
|
||||
.SetValue(options.eventsBasedObjectType);
|
||||
initialRuntimeGameStatus.AddChild("eventsBasedObjectVariantName")
|
||||
.SetValue(options.eventsBasedObjectVariantName);
|
||||
}
|
||||
|
||||
runtimeGameOptions.AddChild("shouldReloadLibraries")
|
||||
.SetBoolValue(options.shouldReloadLibraries);
|
||||
runtimeGameOptions.AddChild("shouldGenerateScenesEventsCode")
|
||||
.SetBoolValue(options.shouldGenerateScenesEventsCode);
|
||||
|
||||
runtimeGameOptions.AddChild("nativeMobileApp")
|
||||
.SetBoolValue(options.nativeMobileApp);
|
||||
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
|
||||
@@ -297,71 +469,106 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
|
||||
for (const auto &includeFile : includesFiles) {
|
||||
auto hashIt = options.includeFileHashes.find(includeFile);
|
||||
gd::String scriptSrc = GetExportedIncludeFilename(includeFile);
|
||||
gd::String scriptSrc = GetExportedIncludeFilename(fs, gdjsRoot, 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 (!ExportIndexFile(exportedProject,
|
||||
gdjsRoot + "/Runtime/index.html",
|
||||
options.exportPath,
|
||||
includesFiles,
|
||||
usedExtensionsResult.GetUsedSourceFiles(),
|
||||
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));
|
||||
void ExporterHelper::AddInGameEditorResources(
|
||||
gd::Project &project, std::set<gd::String> &projectUsedResources) {
|
||||
project.GetResourcesManager().AddResource(
|
||||
"InGameEditor-ShapePainterIcon",
|
||||
"InGameEditor/Resources/primitivedrawingicon.png", "image");
|
||||
projectUsedResources.insert("InGameEditor-ShapePainterIcon");
|
||||
}
|
||||
|
||||
void ExporterHelper::SerializeProjectData(gd::AbstractFileSystem &fs,
|
||||
const gd::Project &project,
|
||||
const PreviewExportOptions &options,
|
||||
gd::SerializerElement &rootElement) {
|
||||
gd::Project clonedProject = project;
|
||||
|
||||
// Replace all resource file paths with the one used in exported projects.
|
||||
auto projectDirectory = fs.DirNameFrom(project.GetProjectFile());
|
||||
gd::ResourcesMergingHelper resourcesMergingHelper(
|
||||
clonedProject.GetResourcesManager(), fs);
|
||||
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
|
||||
if (options.isInGameEdition) {
|
||||
resourcesMergingHelper.SetShouldUseOriginalAbsoluteFilenames();
|
||||
} else {
|
||||
resourcesMergingHelper.PreserveDirectoriesStructure(false);
|
||||
resourcesMergingHelper.PreserveAbsoluteFilenames(false);
|
||||
}
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(clonedProject,
|
||||
resourcesMergingHelper);
|
||||
|
||||
ExporterHelper::StriptAndSerializeProjectData(
|
||||
clonedProject, rootElement, options.isInGameEdition);
|
||||
}
|
||||
|
||||
void ExporterHelper::StriptAndSerializeProjectData(
|
||||
gd::Project &project, gd::SerializerElement &rootElement,
|
||||
bool isInGameEdition) {
|
||||
auto projectUsedResources =
|
||||
gd::SceneResourcesFinder::FindProjectResources(project);
|
||||
if (isInGameEdition) {
|
||||
ExporterHelper::AddInGameEditorResources(project, projectUsedResources);
|
||||
}
|
||||
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
|
||||
for (std::size_t layoutIndex = 0;
|
||||
layoutIndex < project.GetLayoutsCount(); layoutIndex++) {
|
||||
auto &layout = project.GetLayout(layoutIndex);
|
||||
scenesUsedResources[layout.GetName()] =
|
||||
gd::SceneResourcesFinder::FindSceneResources(project, layout);
|
||||
}
|
||||
std::unordered_map<gd::String, std::set<gd::String>>
|
||||
eventsBasedObjectVariantsUsedResources;
|
||||
for (std::size_t extensionIndex = 0;
|
||||
extensionIndex < project.GetEventsFunctionsExtensionsCount();
|
||||
extensionIndex++) {
|
||||
auto &eventsFunctionsExtension =
|
||||
project.GetEventsFunctionsExtension(extensionIndex);
|
||||
for (auto &&eventsBasedObject :
|
||||
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
|
||||
|
||||
auto eventsBasedObjectType = gd::PlatformExtension::GetObjectFullType(
|
||||
eventsFunctionsExtension.GetName(), eventsBasedObject->GetName());
|
||||
eventsBasedObjectVariantsUsedResources[eventsBasedObjectType] =
|
||||
gd::SceneResourcesFinder::FindEventsBasedObjectVariantResources(
|
||||
project, eventsBasedObject->GetDefaultVariant());
|
||||
|
||||
for (auto &&eventsBasedObjectVariant :
|
||||
eventsBasedObject->GetVariants().GetInternalVector()) {
|
||||
|
||||
auto variantType = gd::PlatformExtension::GetVariantFullType(
|
||||
eventsFunctionsExtension.GetName(), eventsBasedObject->GetName(),
|
||||
eventsBasedObjectVariant->GetName());
|
||||
eventsBasedObjectVariantsUsedResources[variantType] =
|
||||
gd::SceneResourcesFinder::FindEventsBasedObjectVariantResources(
|
||||
project, *eventsBasedObjectVariant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip the project (*after* generating events as the events may use stripped
|
||||
// things (objects groups...))
|
||||
gd::ProjectStripper::StripProjectForExport(project);
|
||||
|
||||
// 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 "";
|
||||
SerializeUsedResources(rootElement, projectUsedResources, scenesUsedResources,
|
||||
eventsBasedObjectVariantsUsedResources);
|
||||
}
|
||||
|
||||
void ExporterHelper::SerializeUsedResources(
|
||||
gd::SerializerElement &rootElement,
|
||||
std::set<gd::String> &projectUsedResources,
|
||||
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
|
||||
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources,
|
||||
std::unordered_map<gd::String, std::set<gd::String>>
|
||||
&eventsBasedObjectVariantsUsedResources) {
|
||||
auto serializeUsedResources =
|
||||
[](gd::SerializerElement &element,
|
||||
std::set<gd::String> &usedResources) -> void {
|
||||
@@ -385,6 +592,41 @@ void ExporterHelper::SerializeUsedResources(
|
||||
auto &layoutUsedResources = scenesUsedResources[layoutName];
|
||||
serializeUsedResources(layoutElement, layoutUsedResources);
|
||||
}
|
||||
|
||||
auto &extensionsElement = rootElement.GetChild("eventsFunctionsExtensions");
|
||||
for (std::size_t extensionIndex = 0;
|
||||
extensionIndex < extensionsElement.GetChildrenCount();
|
||||
extensionIndex++) {
|
||||
auto &extensionElement = extensionsElement.GetChild(extensionIndex);
|
||||
const auto extensionName = extensionElement.GetStringAttribute("name");
|
||||
|
||||
auto &objectsElement = extensionElement.GetChild("eventsBasedObjects");
|
||||
|
||||
for (std::size_t objectIndex = 0;
|
||||
objectIndex < objectsElement.GetChildrenCount(); objectIndex++) {
|
||||
auto &objectElement = objectsElement.GetChild(objectIndex);
|
||||
const auto objectName = objectElement.GetStringAttribute("name");
|
||||
|
||||
auto eventsBasedObjectType =
|
||||
gd::PlatformExtension::GetObjectFullType(extensionName, objectName);
|
||||
auto &objectUsedResources =
|
||||
eventsBasedObjectVariantsUsedResources[eventsBasedObjectType];
|
||||
serializeUsedResources(objectElement, objectUsedResources);
|
||||
|
||||
auto &variantsElement = objectElement.GetChild("variants");
|
||||
for (std::size_t variantIndex = 0;
|
||||
variantIndex < variantsElement.GetChildrenCount(); variantIndex++) {
|
||||
auto &variantElement = variantsElement.GetChild(variantIndex);
|
||||
const auto variantName = variantElement.GetStringAttribute("name");
|
||||
|
||||
auto variantType = gd::PlatformExtension::GetVariantFullType(
|
||||
extensionName, objectName, variantName);
|
||||
auto &variantUsedResources =
|
||||
eventsBasedObjectVariantsUsedResources[variantType];
|
||||
serializeUsedResources(variantElement, variantUsedResources);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ExporterHelper::ExportIndexFile(
|
||||
@@ -775,7 +1017,7 @@ bool ExporterHelper::CompleteIndexFile(
|
||||
gd::String codeFilesIncludes;
|
||||
for (auto &include : includesFiles) {
|
||||
gd::String scriptSrc =
|
||||
GetExportedIncludeFilename(include, nonRuntimeScriptsCacheBurst);
|
||||
GetExportedIncludeFilename(fs, gdjsRoot, include, nonRuntimeScriptsCacheBurst);
|
||||
|
||||
// Sanity check if the file exists - if not skip it to avoid
|
||||
// including it in the list of scripts.
|
||||
@@ -801,6 +1043,7 @@ bool ExporterHelper::CompleteIndexFile(
|
||||
|
||||
void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
bool pixiInThreeRenderers,
|
||||
bool isInGameEdition,
|
||||
bool includeWebsocketDebuggerClient,
|
||||
bool includeWindowMessageDebuggerClient,
|
||||
bool includeMinimalDebuggerClient,
|
||||
@@ -878,6 +1121,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
|
||||
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
|
||||
InsertUnique(includesFiles, "debugger-client/InGameDebugger.js");
|
||||
InsertUnique(includesFiles, "InGameEditor/InGameEditor.js");
|
||||
}
|
||||
if (includeWebsocketDebuggerClient) {
|
||||
InsertUnique(includesFiles, "debugger-client/websocket-debugger-client.js");
|
||||
@@ -890,14 +1134,16 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
InsertUnique(includesFiles, "debugger-client/minimal-debugger-client.js");
|
||||
}
|
||||
|
||||
if (pixiInThreeRenderers) {
|
||||
if (pixiInThreeRenderers || isInGameEdition) {
|
||||
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");
|
||||
// Extensions in JS may use it.
|
||||
InsertUnique(includesFiles, "Extensions/3D/Scene3DTools.js");
|
||||
}
|
||||
if (pixiRenderers) {
|
||||
if (pixiRenderers || isInGameEdition) {
|
||||
InsertUnique(includesFiles, "pixi-renderers/pixi.js");
|
||||
InsertUnique(includesFiles, "pixi-renderers/pixi-filters-tools.js");
|
||||
InsertUnique(includesFiles, "pixi-renderers/runtimegame-pixi-renderer.js");
|
||||
@@ -921,7 +1167,12 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
|
||||
includesFiles,
|
||||
"fontfaceobserver-font-manager/fontfaceobserver-font-manager.js");
|
||||
}
|
||||
if (pixiInThreeRenderers) {
|
||||
if (isInGameEdition) {
|
||||
// `InGameEditor` uses the `is3D` function.
|
||||
InsertUnique(includesFiles, "Extensions/3D/Base3DBehavior.js");
|
||||
InsertUnique(includesFiles, "Extensions/3D/HemisphereLight.js");
|
||||
}
|
||||
if (pixiInThreeRenderers || isInGameEdition) {
|
||||
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3D.js");
|
||||
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3DRenderer.js");
|
||||
InsertUnique(includesFiles, "Extensions/3D/CustomRuntimeObject3D.js");
|
||||
@@ -959,7 +1210,7 @@ bool ExporterHelper::ExportEffectIncludes(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExporterHelper::ExportEventsCode(
|
||||
bool ExporterHelper::ExportScenesEventsCode(
|
||||
const gd::Project &project,
|
||||
gd::String outputDir,
|
||||
std::vector<gd::String> &includesFiles,
|
||||
@@ -995,6 +1246,7 @@ bool ExporterHelper::ExportEventsCode(
|
||||
}
|
||||
|
||||
gd::String ExporterHelper::GetExportedIncludeFilename(
|
||||
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
|
||||
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst) {
|
||||
auto addSearchParameterToUrl = [](const gd::String &url,
|
||||
const gd::String &urlEncodedParameterName,
|
||||
|
@@ -42,9 +42,9 @@ struct PreviewExportOptions {
|
||||
useWindowMessageDebuggerClient(false),
|
||||
useMinimalDebuggerClient(false),
|
||||
nativeMobileApp(false),
|
||||
projectDataOnlyExport(false),
|
||||
fullLoadingScreen(false),
|
||||
isDevelopmentEnvironment(false),
|
||||
isInGameEdition(false),
|
||||
nonRuntimeScriptsCacheBurst(0),
|
||||
inAppTutorialMessageInPreview(""),
|
||||
inAppTutorialMessagePositionInPreview(""),
|
||||
@@ -145,6 +145,26 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the (optional) events-based object to be instantiated in the scene
|
||||
* at the beginning of the previewed game.
|
||||
*/
|
||||
PreviewExportOptions &SetEventsBasedObjectType(
|
||||
const gd::String &eventsBasedObjectType_) {
|
||||
eventsBasedObjectType = eventsBasedObjectType_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the (optional) events-based object variant to be instantiated in the scene
|
||||
* at the beginning of the previewed game.
|
||||
*/
|
||||
PreviewExportOptions &SetEventsBasedObjectVariantName(
|
||||
const gd::String &eventsBasedObjectVariantName_) {
|
||||
eventsBasedObjectVariantName = eventsBasedObjectVariantName_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the hash associated to an include file. Useful for the preview
|
||||
* hot-reload, to know if a file changed.
|
||||
@@ -156,11 +176,34 @@ struct PreviewExportOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the export should only export the project data, not
|
||||
* exporting events code.
|
||||
* \brief Set if the exported folder should be cleared befor the export.
|
||||
*/
|
||||
PreviewExportOptions &SetProjectDataOnlyExport(bool enable) {
|
||||
projectDataOnlyExport = enable;
|
||||
PreviewExportOptions &SetShouldClearExportFolder(bool enable) {
|
||||
shouldClearExportFolder = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the `ProjectData` must be reloaded.
|
||||
*/
|
||||
PreviewExportOptions &SetShouldReloadProjectData(bool enable) {
|
||||
shouldReloadProjectData = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if GDJS libraries must be reloaded.
|
||||
*/
|
||||
PreviewExportOptions &SetShouldReloadLibraries(bool enable) {
|
||||
shouldReloadLibraries = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the export should export events code.
|
||||
*/
|
||||
PreviewExportOptions &SetShouldGenerateScenesEventsCode(bool enable) {
|
||||
shouldGenerateScenesEventsCode = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -182,6 +225,40 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the export is made for being edited in the editor.
|
||||
*/
|
||||
PreviewExportOptions &SetIsInGameEdition(bool enable) {
|
||||
isInGameEdition = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the in-game editor identifier.
|
||||
*/
|
||||
PreviewExportOptions &SetEditorId(const gd::String &editorId_) {
|
||||
editorId = editorId_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set the camera state to use in the in-game editor.
|
||||
*/
|
||||
PreviewExportOptions &
|
||||
SetEditorCameraState3D(const gd::String &cameraMode, double positionX,
|
||||
double positionY, double positionZ,
|
||||
double rotationAngle, double elevationAngle,
|
||||
double distance) {
|
||||
editorCamera3DCameraMode = cameraMode;
|
||||
editorCamera3DPositionX = positionX;
|
||||
editorCamera3DPositionY = positionY;
|
||||
editorCamera3DPositionZ = positionZ;
|
||||
editorCamera3DRotationAngle = rotationAngle;
|
||||
editorCamera3DElevationAngle = elevationAngle;
|
||||
editorCamera3DDistance = distance;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief If set to a non zero value, the exported script URLs will have an
|
||||
* extra search parameter added (with the given value) to ensure browser cache
|
||||
@@ -294,6 +371,8 @@ struct PreviewExportOptions {
|
||||
bool useMinimalDebuggerClient;
|
||||
gd::String layoutName;
|
||||
gd::String externalLayoutName;
|
||||
gd::String eventsBasedObjectType;
|
||||
gd::String eventsBasedObjectVariantName;
|
||||
gd::String fallbackAuthorUsername;
|
||||
gd::String fallbackAuthorId;
|
||||
gd::String playerId;
|
||||
@@ -303,9 +382,21 @@ struct PreviewExportOptions {
|
||||
gd::String inAppTutorialMessagePositionInPreview;
|
||||
bool nativeMobileApp;
|
||||
std::map<gd::String, int> includeFileHashes;
|
||||
bool projectDataOnlyExport;
|
||||
bool shouldClearExportFolder = true;
|
||||
bool shouldReloadProjectData = true;
|
||||
bool shouldReloadLibraries = true;
|
||||
bool shouldGenerateScenesEventsCode = true;
|
||||
bool fullLoadingScreen;
|
||||
bool isDevelopmentEnvironment;
|
||||
bool isInGameEdition;
|
||||
gd::String editorId;
|
||||
gd::String editorCamera3DCameraMode;
|
||||
double editorCamera3DPositionX = 0;
|
||||
double editorCamera3DPositionY = 0;
|
||||
double editorCamera3DPositionZ = 0;
|
||||
double editorCamera3DRotationAngle = 0;
|
||||
double editorCamera3DElevationAngle = 0;
|
||||
double editorCamera3DDistance = 0;
|
||||
unsigned int nonRuntimeScriptsCacheBurst;
|
||||
gd::String electronRemoteRequirePath;
|
||||
gd::String gdevelopResourceToken;
|
||||
@@ -379,23 +470,51 @@ class ExporterHelper {
|
||||
const gd::String &GetLastError() const { return lastError; };
|
||||
|
||||
/**
|
||||
* \brief Export a project to JSON
|
||||
* \brief Export a project without its events and options to 2 JS variables
|
||||
*
|
||||
* \param fs The abstract file system to use to write the file
|
||||
* \param project The project to be exported.
|
||||
* \param filename The filename where export the project
|
||||
* \param runtimeGameOptions The content of the extra configuration to store
|
||||
* in gdjs.runtimeGameOptions \return Empty string if everything is ok,
|
||||
* in gdjs.runtimeGameOptions
|
||||
*
|
||||
* \return Empty string if everything is ok,
|
||||
* description of the error otherwise.
|
||||
*/
|
||||
static gd::String 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>>
|
||||
&layersUsedResources);
|
||||
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
|
||||
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition);
|
||||
|
||||
/**
|
||||
* \brief Serialize a project without its events to JSON
|
||||
*
|
||||
* \param fs The abstract file system to use to write the file
|
||||
* \param project The project to be exported.
|
||||
* \param options The content of the extra configuration
|
||||
* \param projectDataElement The element where the project data is serialized
|
||||
*/
|
||||
static void SerializeProjectData(gd::AbstractFileSystem &fs,
|
||||
const gd::Project &project,
|
||||
const PreviewExportOptions &options,
|
||||
gd::SerializerElement &projectDataElement);
|
||||
|
||||
/**
|
||||
* \brief Serialize the content of the extra configuration to store
|
||||
* in gdjs.runtimeGameOptions to JSON
|
||||
*
|
||||
* \param fs The abstract file system to use to write the file
|
||||
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
|
||||
* \param options The content of the extra configuration
|
||||
* \param includesFiles The list of scripts files - useful for hot-reloading
|
||||
* \param runtimeGameOptionsElement The element where the game options are
|
||||
* serialized
|
||||
*/
|
||||
static void
|
||||
SerializeRuntimeGameOptions(gd::AbstractFileSystem &fs,
|
||||
const gd::String &gdjsRoot,
|
||||
const PreviewExportOptions &options,
|
||||
std::vector<gd::String> &includesFiles,
|
||||
gd::SerializerElement &runtimeGameOptionsElement);
|
||||
|
||||
/**
|
||||
* \brief Copy all the resources of the project to to the export directory,
|
||||
@@ -416,6 +535,7 @@ class ExporterHelper {
|
||||
*/
|
||||
void AddLibsInclude(bool pixiRenderers,
|
||||
bool pixiInThreeRenderers,
|
||||
bool isInGameEdition,
|
||||
bool includeWebsocketDebuggerClient,
|
||||
bool includeWindowMessageDebuggerClient,
|
||||
bool includeMinimalDebuggerClient,
|
||||
@@ -453,7 +573,7 @@ class ExporterHelper {
|
||||
* includesFiles A reference to a vector that will be filled with JS files to
|
||||
* be exported along with the project. ( including "codeX.js" files ).
|
||||
*/
|
||||
bool ExportEventsCode(
|
||||
bool ExportScenesEventsCode(
|
||||
const gd::Project &project,
|
||||
gd::String outputDir,
|
||||
std::vector<gd::String> &includesFiles,
|
||||
@@ -578,14 +698,20 @@ class ExporterHelper {
|
||||
* a browser pointing to the preview.
|
||||
*
|
||||
* \param options The options to generate the preview.
|
||||
* \param includesFiles The list of scripts files - useful for hot-reloading
|
||||
*/
|
||||
bool ExportProjectForPixiPreview(const PreviewExportOptions &options);
|
||||
bool ExportProjectForPixiPreview(const PreviewExportOptions &options,
|
||||
std::vector<gd::String> &includesFiles);
|
||||
|
||||
/**
|
||||
* \brief Given an include file, returns the name of the file to reference
|
||||
* in the exported game.
|
||||
*
|
||||
* \param fs The abstract file system to use
|
||||
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
|
||||
*/
|
||||
gd::String GetExportedIncludeFilename(
|
||||
static gd::String GetExportedIncludeFilename(
|
||||
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
|
||||
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst = 0);
|
||||
|
||||
/**
|
||||
@@ -612,11 +738,36 @@ class ExporterHelper {
|
||||
///< be then copied to the final output directory.
|
||||
|
||||
private:
|
||||
static void SerializeUsedResources(
|
||||
gd::SerializerElement &rootElement,
|
||||
std::set<gd::String> &projectUsedResources,
|
||||
std::unordered_map<gd::String, std::set<gd::String>>
|
||||
&layersUsedResources);
|
||||
static void
|
||||
SerializeUsedResources(gd::SerializerElement &rootElement,
|
||||
std::set<gd::String> &projectUsedResources,
|
||||
std::unordered_map<gd::String, std::set<gd::String>>
|
||||
&layersUsedResources,
|
||||
std::unordered_map<gd::String, std::set<gd::String>>
|
||||
&eventsBasedObjectVariantsUsedResources);
|
||||
|
||||
/**
|
||||
* \brief Stript a project and serialize it to JSON
|
||||
*
|
||||
* \param project The project to be exported.
|
||||
*/
|
||||
static void StriptAndSerializeProjectData(gd::Project &project,
|
||||
gd::SerializerElement &rootElement,
|
||||
bool isInGameEdition);
|
||||
|
||||
/**
|
||||
* \brief Add additional resources that are used by the in-game editor to the
|
||||
* project.
|
||||
*
|
||||
* \param project The project to be exported where resource declarations are
|
||||
* added.
|
||||
*
|
||||
* \param projectUsedResources The list of resource to be loaded
|
||||
* globally by the runtime.
|
||||
*/
|
||||
static void
|
||||
AddInGameEditorResources(gd::Project &project,
|
||||
std::set<gd::String> &projectUsedResources);
|
||||
};
|
||||
|
||||
} // namespace gdjs
|
||||
|
@@ -100,18 +100,15 @@ namespace gdjs {
|
||||
getNetworkSyncData(
|
||||
syncOptions: GetNetworkSyncDataOptions
|
||||
): AsyncTasksManagerNetworkSyncData {
|
||||
const tasksData = this.tasksWithCallback
|
||||
.map(({ asyncTask, callbackId, longLivedObjectsList }) => {
|
||||
// Stay compatible with old extensions declaring tasks without a getNetworkSyncData method.
|
||||
if (!asyncTask.getNetworkSyncData) return null;
|
||||
|
||||
const tasksData = this.tasksWithCallback.map(
|
||||
({ asyncTask, callbackId, longLivedObjectsList }) => {
|
||||
return {
|
||||
callbackId,
|
||||
asyncTask: asyncTask.getNetworkSyncData(),
|
||||
objectsList: longLivedObjectsList.getNetworkSyncData(syncOptions),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
tasks: tasksData,
|
||||
|
@@ -13,7 +13,7 @@ namespace gdjs {
|
||||
export type CustomObjectConfiguration = ObjectConfiguration & {
|
||||
animatable?: SpriteAnimationData[];
|
||||
variant: string;
|
||||
childrenContent: { [objectName: string]: ObjectConfiguration & any };
|
||||
childrenContent?: { [objectName: string]: ObjectConfiguration & any };
|
||||
isInnerAreaFollowingParentSize: boolean;
|
||||
};
|
||||
|
||||
@@ -118,37 +118,19 @@ namespace gdjs {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventsBasedObjectData.defaultVariant) {
|
||||
eventsBasedObjectData.defaultVariant = {
|
||||
...eventsBasedObjectData,
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
// Legacy events-based objects don't have any instance in their default
|
||||
// variant since there wasn't a graphical editor at the time.
|
||||
// In this case, the editor doesn't allow to choose a variant, but a
|
||||
// variant may have stayed after a user rolled back the extension.
|
||||
// This variant must be ignored to match what the editor shows.
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
eventsBasedObjectData.defaultVariant.instances.length == 0;
|
||||
let usedVariantData: EventsBasedObjectVariantData =
|
||||
eventsBasedObjectData.defaultVariant;
|
||||
if (
|
||||
customObjectData.variant &&
|
||||
!isForcedToOverrideEventsBasedObjectChildrenConfiguration
|
||||
) {
|
||||
for (
|
||||
let variantIndex = 0;
|
||||
variantIndex < eventsBasedObjectData.variants.length;
|
||||
variantIndex++
|
||||
) {
|
||||
const variantData = eventsBasedObjectData.variants[variantIndex];
|
||||
if (variantData.name === customObjectData.variant) {
|
||||
usedVariantData = variantData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const usedVariantData: EventsBasedObjectVariantData | null =
|
||||
this.getRuntimeScene()
|
||||
.getGame()
|
||||
.getEventsBasedObjectVariantData(
|
||||
customObjectData.type,
|
||||
customObjectData.variant
|
||||
);
|
||||
if (!usedVariantData) {
|
||||
// This can't actually happen.
|
||||
logger.error(
|
||||
`Unknown variant "${customObjectData.variant}" for object "${customObjectData.type}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._isInnerAreaFollowingParentSize =
|
||||
@@ -178,8 +160,7 @@ namespace gdjs {
|
||||
override reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
|
||||
super.reinitialize(objectData);
|
||||
|
||||
this._reinitializeRenderer();
|
||||
this._initializeFromObjectData(objectData);
|
||||
this._reinitializeContentFromObjectData(objectData);
|
||||
|
||||
// When changing the variant, the instance is like a new instance.
|
||||
// We call again `onCreated` at the end, like done by the constructor
|
||||
@@ -187,6 +168,14 @@ namespace gdjs {
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
private _reinitializeContentFromObjectData(
|
||||
objectData: ObjectData & CustomObjectConfiguration
|
||||
) {
|
||||
this._reinitializeRenderer();
|
||||
this._instanceContainer._unloadContent();
|
||||
this._initializeFromObjectData(objectData);
|
||||
}
|
||||
|
||||
override updateFromObjectData(
|
||||
oldObjectData: ObjectData & CustomObjectConfiguration,
|
||||
newObjectData: ObjectData & CustomObjectConfiguration
|
||||
@@ -214,8 +203,7 @@ namespace gdjs {
|
||||
this._instanceContainer._initialInnerArea.max[1] !==
|
||||
this._innerArea.max[1]);
|
||||
|
||||
this._reinitializeRenderer();
|
||||
this._initializeFromObjectData(newObjectData);
|
||||
this._reinitializeContentFromObjectData(newObjectData);
|
||||
|
||||
// The generated code calls the onCreated super implementation at the end.
|
||||
this.onCreated();
|
||||
@@ -311,15 +299,13 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
}
|
||||
|
||||
override onDeletedFromScene(): void {
|
||||
@@ -658,6 +644,20 @@ namespace gdjs {
|
||||
return this._unrotatedAABB.max[1];
|
||||
}
|
||||
|
||||
getOriginalWidth(): float {
|
||||
return (
|
||||
this._instanceContainer.getInitialUnrotatedViewportMaxX() -
|
||||
this._instanceContainer.getInitialUnrotatedViewportMinX()
|
||||
);
|
||||
}
|
||||
|
||||
getOriginalHeight(): float {
|
||||
return (
|
||||
this._instanceContainer.getInitialUnrotatedViewportMaxY() -
|
||||
this._instanceContainer.getInitialUnrotatedViewportMinY()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the internal width of the object according to its children.
|
||||
*/
|
||||
|
@@ -16,6 +16,7 @@ namespace gdjs {
|
||||
_parent: gdjs.RuntimeInstanceContainer;
|
||||
/** The object that is built with the instances of this container. */
|
||||
_customObject: gdjs.CustomRuntimeObject;
|
||||
// TODO Remove this attribute
|
||||
_isLoaded: boolean = false;
|
||||
/**
|
||||
* The default size defined by users in the custom object initial instances editor.
|
||||
@@ -46,15 +47,28 @@ namespace gdjs {
|
||||
this._debuggerRenderer = new gdjs.DebuggerRenderer(this);
|
||||
}
|
||||
|
||||
// TODO `_layers` and `_orderedLayers` should not be used directly.
|
||||
|
||||
addLayer(layerData: LayerData) {
|
||||
if (this._layers.containsKey(layerData.name)) {
|
||||
return;
|
||||
}
|
||||
// This code is duplicated with `RuntimeScene.addLayer` because it avoids
|
||||
// to expose a method to build a layer.
|
||||
const layer = new gdjs.RuntimeCustomObjectLayer(layerData, this);
|
||||
this._layers.put(layerData.name, layer);
|
||||
this._orderedLayers.push(layer);
|
||||
}
|
||||
|
||||
_unloadContent() {
|
||||
this.onDeletedFromScene(this._parent);
|
||||
// At this point, layer renderers are already removed by
|
||||
// `CustomRuntimeObject._reinitializeRenderer`.
|
||||
// It's not great to do this here, but it allows to keep it private.
|
||||
this._layers.clear();
|
||||
this._orderedLayers.length = 0;
|
||||
}
|
||||
|
||||
createObject(objectName: string): gdjs.RuntimeObject | null {
|
||||
const result = super.createObject(objectName);
|
||||
this._customObject.onChildrenLocationChanged();
|
||||
@@ -63,21 +77,14 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Load the container from the given initial configuration.
|
||||
* @param customObjectData An object containing the container data.
|
||||
* @param customObjectData An object containing the parent object data.
|
||||
* @param eventsBasedObjectVariantData An object containing the container data.
|
||||
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
||||
*/
|
||||
loadFrom(
|
||||
customObjectData: ObjectData & CustomObjectConfiguration,
|
||||
eventsBasedObjectVariantData: EventsBasedObjectVariantData
|
||||
) {
|
||||
if (this._isLoaded) {
|
||||
this.onDeletedFromScene(this._parent);
|
||||
}
|
||||
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
!eventsBasedObjectVariantData.name &&
|
||||
eventsBasedObjectVariantData.instances.length == 0;
|
||||
|
||||
this._setOriginalInnerArea(eventsBasedObjectVariantData);
|
||||
|
||||
// Registering objects
|
||||
@@ -87,19 +94,21 @@ namespace gdjs {
|
||||
++i
|
||||
) {
|
||||
const childObjectData = eventsBasedObjectVariantData.objects[i];
|
||||
// The children configuration override only applies to the default variant.
|
||||
if (
|
||||
customObjectData.childrenContent &&
|
||||
(!eventsBasedObjectVariantData.name ||
|
||||
isForcedToOverrideEventsBasedObjectChildrenConfiguration)
|
||||
gdjs.CustomRuntimeObjectInstanceContainer.hasChildrenConfigurationOverriding(
|
||||
customObjectData,
|
||||
eventsBasedObjectVariantData
|
||||
)
|
||||
) {
|
||||
this.registerObject({
|
||||
...childObjectData,
|
||||
// The custom object overrides its events-based object configuration.
|
||||
// The custom object overrides its variant configuration with
|
||||
// a legacy children configuration.
|
||||
...customObjectData.childrenContent[childObjectData.name],
|
||||
});
|
||||
} else {
|
||||
// The custom object follows its events-based object configuration.
|
||||
// The custom object follows its variant configuration.
|
||||
this.registerObject(childObjectData);
|
||||
}
|
||||
}
|
||||
@@ -154,6 +163,28 @@ namespace gdjs {
|
||||
this._isLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the custom object has a children configuration overriding that
|
||||
* should be used instead of the variant's objects configurations.
|
||||
* @param customObjectData An object containing the parent object data.
|
||||
* @param eventsBasedObjectVariantData An object containing the container data.
|
||||
* @returns
|
||||
*/
|
||||
static hasChildrenConfigurationOverriding(
|
||||
customObjectData: CustomObjectConfiguration,
|
||||
eventsBasedObjectVariantData: EventsBasedObjectVariantData
|
||||
): boolean {
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
!eventsBasedObjectVariantData.name &&
|
||||
eventsBasedObjectVariantData.instances.length == 0;
|
||||
|
||||
// The children configuration override only applies to the default variant.
|
||||
return customObjectData.childrenContent
|
||||
? !eventsBasedObjectVariantData.name ||
|
||||
isForcedToOverrideEventsBasedObjectChildrenConfiguration
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize `_initialInnerArea` if it doesn't exist.
|
||||
* `_initialInnerArea` is shared by every instance to save memory.
|
||||
@@ -161,7 +192,10 @@ namespace gdjs {
|
||||
private _setOriginalInnerArea(
|
||||
eventsBasedObjectData: EventsBasedObjectVariantData
|
||||
) {
|
||||
if (eventsBasedObjectData.instances.length > 0) {
|
||||
if (
|
||||
eventsBasedObjectData.instances.length > 0 ||
|
||||
this.getGame().isInGameEdition()
|
||||
) {
|
||||
if (!eventsBasedObjectData._initialInnerArea) {
|
||||
eventsBasedObjectData._initialInnerArea = {
|
||||
min: [
|
||||
@@ -341,6 +375,12 @@ namespace gdjs {
|
||||
return this._initialInnerArea ? this._initialInnerArea.max[1] : 0;
|
||||
}
|
||||
|
||||
_getInitialInnerAreaDepth(): float {
|
||||
return this._initialInnerArea
|
||||
? this._initialInnerArea.max[2] - this._initialInnerArea.min[2]
|
||||
: 0;
|
||||
}
|
||||
|
||||
getViewportWidth(): float {
|
||||
return this._customObject.getUnscaledWidth();
|
||||
}
|
||||
|
4068
GDJS/Runtime/InGameEditor/InGameEditor.ts
Normal file
4068
GDJS/Runtime/InGameEditor/InGameEditor.ts
Normal file
File diff suppressed because it is too large
Load Diff
BIN
GDJS/Runtime/InGameEditor/Resources/primitivedrawingicon.png
Normal file
BIN
GDJS/Runtime/InGameEditor/Resources/primitivedrawingicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 394 B |
@@ -164,14 +164,17 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
|
||||
const loadedThreeModel = this._loadedThreeModels.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (loadedThreeModel) {
|
||||
loadedThreeModel.scene.clear();
|
||||
this._loadedThreeModels.delete(resourceData);
|
||||
}
|
||||
|
||||
const downloadedArrayBuffer =
|
||||
this._downloadedArrayBuffers.get(resourceData);
|
||||
const downloadedArrayBuffer = this._downloadedArrayBuffers.getFromName(
|
||||
resourceData.name
|
||||
);
|
||||
if (downloadedArrayBuffer) {
|
||||
this._downloadedArrayBuffers.delete(resourceData);
|
||||
}
|
||||
|
@@ -282,6 +282,27 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
async loadResources(
|
||||
resourceNames: Array<string>,
|
||||
onProgress: (loadingCount: integer, totalCount: integer) => void
|
||||
): Promise<void> {
|
||||
let loadedCount = 0;
|
||||
await processAndRetryIfNeededWithPromisePool(
|
||||
resourceNames,
|
||||
maxForegroundConcurrency,
|
||||
maxAttempt,
|
||||
async (resourceName) => {
|
||||
const resource = this._resources.get(resourceName);
|
||||
if (resource) {
|
||||
await this._loadResource(resource);
|
||||
await this._processResource(resource);
|
||||
}
|
||||
loadedCount++;
|
||||
onProgress(loadedCount, this._resources.size);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the resources that are needed to launch the first scene.
|
||||
*/
|
||||
@@ -551,6 +572,23 @@ namespace gdjs {
|
||||
// TODO: mark the scene as unloaded so it's not automatically loaded again eagerly.
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when hot-reloading resources.
|
||||
*/
|
||||
unloadAllResources(): void {
|
||||
debugLogger.log(`Unloading of all resources was requested.`);
|
||||
for (const resource of this._resources.values()) {
|
||||
const resourceManager = this._resourceManagersMap.get(resource.kind);
|
||||
if (resourceManager) {
|
||||
resourceManager.unloadResource(resource);
|
||||
}
|
||||
}
|
||||
for (const sceneLoadingState of this._sceneLoadingStates.values()) {
|
||||
sceneLoadingState.status = 'not-loaded';
|
||||
}
|
||||
debugLogger.log(`Unloading of all resources finished.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a given scene at the end of the queue.
|
||||
*
|
||||
@@ -652,6 +690,9 @@ namespace gdjs {
|
||||
* the resource (this can be for example a token needed to access the resource).
|
||||
*/
|
||||
getFullUrl(url: string) {
|
||||
if (this._runtimeGame.isInGameEdition()) {
|
||||
url = addSearchParameterToUrl(url, 'cache', '' + Date.now());
|
||||
}
|
||||
const { gdevelopResourceToken } = this._runtimeGame._options;
|
||||
if (!gdevelopResourceToken) return url;
|
||||
|
||||
|
@@ -312,15 +312,15 @@ namespace gdjs {
|
||||
xPos: float,
|
||||
yPos: float,
|
||||
zPos: float,
|
||||
trackByPersistentUuid: boolean,
|
||||
options?: {
|
||||
excludedObjectNames?: Set<string> | null;
|
||||
}
|
||||
trackByPersistentUuid: boolean
|
||||
): void {
|
||||
let zOffset: number = zPos;
|
||||
let shouldTrackByPersistentUuid: boolean = trackByPersistentUuid;
|
||||
let zOffset: number;
|
||||
let shouldTrackByPersistentUuid: boolean;
|
||||
|
||||
if (arguments.length <= 4) {
|
||||
if (arguments.length === 5) {
|
||||
zOffset = zPos;
|
||||
shouldTrackByPersistentUuid = trackByPersistentUuid;
|
||||
} else {
|
||||
/**
|
||||
* Support for the previous signature (before 3D was introduced):
|
||||
* createObjectsFrom(data, xPos, yPos, trackByPersistentUuid)
|
||||
@@ -332,10 +332,6 @@ namespace gdjs {
|
||||
for (let i = 0, len = data.length; i < len; ++i) {
|
||||
const instanceData = data[i];
|
||||
const objectName = instanceData.name;
|
||||
if (options?.excludedObjectNames?.has(objectName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newObject = this.createObject(objectName);
|
||||
if (newObject !== null) {
|
||||
if (shouldTrackByPersistentUuid) {
|
||||
@@ -579,6 +575,14 @@ namespace gdjs {
|
||||
this._cacheOrClearRemovedInstances();
|
||||
}
|
||||
|
||||
_updateObjectsForInGameEditor() {
|
||||
const allInstancesList = this.getAdhocListOfAllInstances();
|
||||
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
||||
const obj = allInstancesList[i];
|
||||
obj.update(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call each behavior stepPostEvents method.
|
||||
*/
|
||||
@@ -613,10 +617,10 @@ namespace gdjs {
|
||||
* @param name Name of the object for which the instances must be returned.
|
||||
* @return The list of objects with the given name
|
||||
*/
|
||||
getObjects(name: string): gdjs.RuntimeObject[] {
|
||||
getObjects(name: string): gdjs.RuntimeObject[] | undefined {
|
||||
if (!this._instances.containsKey(name)) {
|
||||
logger.info(
|
||||
'RuntimeScene.getObjects: No instances called "' +
|
||||
'RuntimeInstanceContainer.getObjects: No instances called "' +
|
||||
name +
|
||||
'"! Adding it.'
|
||||
);
|
||||
@@ -640,20 +644,18 @@ namespace gdjs {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectData = this._objects.get(objectName);
|
||||
|
||||
// Create a new object using the object constructor (cached during loading)
|
||||
// and the stored object's data:
|
||||
const cache = this._instancesCache.get(objectName);
|
||||
const ctor = this._objectsCtor.get(objectName);
|
||||
let obj;
|
||||
if (!cache || cache.length === 0) {
|
||||
obj = new ctor(this, objectData);
|
||||
obj = new ctor(this, this._objects.get(objectName));
|
||||
} else {
|
||||
// Reuse an objet destroyed before. If there is an object in the cache,
|
||||
// then it means it does support reinitialization.
|
||||
obj = cache.pop();
|
||||
obj.reinitialize(objectData);
|
||||
obj.reinitialize(this._objects.get(objectName));
|
||||
}
|
||||
this.addObject(obj);
|
||||
return obj;
|
||||
|
@@ -55,13 +55,14 @@ namespace gdjs {
|
||||
_timeScale: float = 1;
|
||||
_defaultZOrder: integer = 0;
|
||||
_hidden: boolean;
|
||||
_initialEffectsData: Array<EffectData>;
|
||||
_initialLayerData: LayerData;
|
||||
|
||||
// TODO EBO Don't store scene layer related data in layers used by custom objects.
|
||||
// (both these 3D settings and the lighting layer properties below).
|
||||
_initialCamera3DFieldOfView: float;
|
||||
_initialCamera3DFarPlaneDistance: float;
|
||||
_initialCamera3DNearPlaneDistance: float;
|
||||
_initialCamera2DPlaneMaxDrawingDistance: float;
|
||||
|
||||
_runtimeScene: gdjs.RuntimeInstanceContainer;
|
||||
_effectsManager: gdjs.EffectsManager;
|
||||
@@ -94,7 +95,9 @@ namespace gdjs {
|
||||
layerData.camera3DNearPlaneDistance || 0.1;
|
||||
this._initialCamera3DFarPlaneDistance =
|
||||
layerData.camera3DFarPlaneDistance || 2000;
|
||||
this._initialEffectsData = layerData.effects || [];
|
||||
this._initialCamera2DPlaneMaxDrawingDistance =
|
||||
layerData.camera2DPlaneMaxDrawingDistance || 5000;
|
||||
this._initialLayerData = layerData;
|
||||
this._runtimeScene = instanceContainer;
|
||||
this._effectsManager = instanceContainer.getGame().getEffectsManager();
|
||||
this._isLightingLayer = layerData.isLightingLayer;
|
||||
@@ -491,6 +494,9 @@ namespace gdjs {
|
||||
getInitialCamera3DFarPlaneDistance(): float {
|
||||
return this._initialCamera3DFarPlaneDistance;
|
||||
}
|
||||
getInitialCamera2DPlaneMaxDrawingDistance(): float {
|
||||
return this._initialCamera2DPlaneMaxDrawingDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the initial effects data for the layer. Only to
|
||||
@@ -498,7 +504,7 @@ namespace gdjs {
|
||||
* @deprecated
|
||||
*/
|
||||
getInitialEffectsData(): EffectData[] {
|
||||
return this._initialEffectsData;
|
||||
return this._initialLayerData.effects || [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -107,8 +107,12 @@ namespace gdjs {
|
||||
exception: Error,
|
||||
runtimeGame: gdjs.RuntimeGame
|
||||
) => {
|
||||
const sceneNames = runtimeGame.getSceneStack().getAllSceneNames();
|
||||
const currentScene = runtimeGame.getSceneStack().getCurrentScene();
|
||||
const currentScene = runtimeGame.isInGameEdition()
|
||||
? runtimeGame.getInGameEditor()?.getCurrentScene()
|
||||
: runtimeGame.getSceneStack().getCurrentScene();
|
||||
const sceneNames = runtimeGame.isInGameEdition()
|
||||
? [currentScene?.getName()]
|
||||
: runtimeGame.getSceneStack().getAllSceneNames();
|
||||
return {
|
||||
type: 'javascript-uncaught-exception',
|
||||
exception,
|
||||
@@ -116,6 +120,7 @@ namespace gdjs {
|
||||
playerId: runtimeGame.getPlayerId(),
|
||||
sessionId: runtimeGame.getSessionId(),
|
||||
isPreview: runtimeGame.isPreview(),
|
||||
isInGameEdition: runtimeGame.isInGameEdition(),
|
||||
gdevelop: {
|
||||
previewContext: runtimeGame.getAdditionalOptions().previewContext,
|
||||
isNativeMobileApp: runtimeGame.getAdditionalOptions().nativeMobileApp,
|
||||
@@ -233,42 +238,250 @@ namespace gdjs {
|
||||
protected handleCommand(data: any) {
|
||||
const that = this;
|
||||
const runtimeGame = this._runtimegame;
|
||||
const inGameEditor = runtimeGame.getInGameEditor();
|
||||
if (!data || !data.command) {
|
||||
// Not a command that's meant to be handled by the debugger, return silently to
|
||||
// avoid polluting the console.
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.command === 'play') {
|
||||
runtimeGame.pause(false);
|
||||
} else if (data.command === 'pause') {
|
||||
runtimeGame.pause(true);
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'refresh') {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'set') {
|
||||
that.set(data.path, data.newValue);
|
||||
} else if (data.command === 'call') {
|
||||
that.call(data.path, data.args);
|
||||
} else if (data.command === 'profiler.start') {
|
||||
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
|
||||
that.sendProfilerOutput(
|
||||
stoppedProfiler.getFramesAverageMeasures(),
|
||||
stoppedProfiler.getStats()
|
||||
try {
|
||||
if (data.command === 'play') {
|
||||
runtimeGame.pause(false);
|
||||
} else if (data.command === 'pause') {
|
||||
runtimeGame.pause(true);
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'refresh') {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'getStatus') {
|
||||
that.sendRuntimeGameStatus();
|
||||
} else if (data.command === 'set') {
|
||||
that.set(data.path, data.newValue);
|
||||
} else if (data.command === 'call') {
|
||||
that.call(data.path, data.args);
|
||||
} else if (data.command === 'profiler.start') {
|
||||
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
|
||||
that.sendProfilerOutput(
|
||||
stoppedProfiler.getFramesAverageMeasures(),
|
||||
stoppedProfiler.getStats()
|
||||
);
|
||||
that.sendProfilerStopped();
|
||||
});
|
||||
that.sendProfilerStarted();
|
||||
} else if (data.command === 'profiler.stop') {
|
||||
runtimeGame.stopCurrentSceneProfiler();
|
||||
} else if (data.command === 'hotReload') {
|
||||
const runtimeGameOptions: RuntimeGameOptions =
|
||||
data.payload.runtimeGameOptions;
|
||||
if (
|
||||
(runtimeGameOptions.initialRuntimeGameStatus?.isInGameEdition ||
|
||||
false) === runtimeGame.isInGameEdition()
|
||||
) {
|
||||
this._hasLoggedUncaughtException = false;
|
||||
that._hotReloader
|
||||
.hotReload({
|
||||
projectData: data.payload.projectData,
|
||||
runtimeGameOptions,
|
||||
shouldReloadResources:
|
||||
data.payload.shouldReloadResources || false,
|
||||
})
|
||||
.then((logs) => {
|
||||
that.sendHotReloaderLogs(logs);
|
||||
});
|
||||
}
|
||||
} else if (data.command === 'hotReloadObjects') {
|
||||
if (inGameEditor) {
|
||||
const editedInstanceContainer =
|
||||
inGameEditor.getEditedInstanceContainer();
|
||||
if (editedInstanceContainer) {
|
||||
that._hotReloader.hotReloadRuntimeSceneObjects(
|
||||
data.payload.updatedObjects,
|
||||
editedInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (data.command === 'hotReloadLayers') {
|
||||
if (inGameEditor) {
|
||||
const editedInstanceContainer =
|
||||
inGameEditor.getEditedInstanceContainer();
|
||||
if (editedInstanceContainer) {
|
||||
inGameEditor.onLayersDataChange(
|
||||
data.payload.layers,
|
||||
data.payload.areEffectsHidden
|
||||
);
|
||||
runtimeGame.getGameData().areEffectsHiddenInEditor =
|
||||
data.payload.areEffectsHidden;
|
||||
that._hotReloader.hotReloadRuntimeSceneLayers(
|
||||
data.payload.layers,
|
||||
editedInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (data.command === 'setBackgroundColor') {
|
||||
if (inGameEditor) {
|
||||
const editedInstanceContainer =
|
||||
inGameEditor.getEditedInstanceContainer();
|
||||
if (editedInstanceContainer) {
|
||||
const backgroundColor = data.payload.backgroundColor;
|
||||
if (
|
||||
backgroundColor &&
|
||||
editedInstanceContainer instanceof gdjs.RuntimeScene
|
||||
) {
|
||||
const sceneData = runtimeGame.getSceneData(
|
||||
editedInstanceContainer.getScene().getName()
|
||||
);
|
||||
if (sceneData) {
|
||||
editedInstanceContainer._backgroundColor =
|
||||
gdjs.rgbToHexNumber(
|
||||
backgroundColor[0],
|
||||
backgroundColor[1],
|
||||
backgroundColor[2]
|
||||
);
|
||||
sceneData.r = backgroundColor[0];
|
||||
sceneData.v = backgroundColor[1];
|
||||
sceneData.b = backgroundColor[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data.command === 'hotReloadAllInstances') {
|
||||
if (inGameEditor) {
|
||||
const editedInstanceContainer =
|
||||
inGameEditor.getEditedInstanceContainer();
|
||||
if (editedInstanceContainer) {
|
||||
that._hotReloader.hotReloadRuntimeInstances(
|
||||
inGameEditor.getEditedInstanceDataList(),
|
||||
data.payload.instances,
|
||||
editedInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (data.command === 'switchForInGameEdition') {
|
||||
if (!this._runtimegame.isInGameEdition()) return;
|
||||
|
||||
const sceneName = data.sceneName || null;
|
||||
const eventsBasedObjectType = data.eventsBasedObjectType || null;
|
||||
if (!sceneName && !eventsBasedObjectType) {
|
||||
logger.warn(
|
||||
'No scene name specified, switchForInGameEdition aborted'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (inGameEditor) {
|
||||
const wasPaused = this._runtimegame.isPaused();
|
||||
this._runtimegame.pause(true);
|
||||
inGameEditor.switchToSceneOrVariant(
|
||||
data.editorId || null,
|
||||
sceneName,
|
||||
data.externalLayoutName || null,
|
||||
eventsBasedObjectType,
|
||||
data.eventsBasedObjectVariantName || null,
|
||||
data.editorCamera3D || null
|
||||
);
|
||||
this._runtimegame.pause(wasPaused);
|
||||
}
|
||||
} else if (data.command === 'setVisibleStatus') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.setVisibleStatus(data.visible);
|
||||
}
|
||||
} else if (data.command === 'updateInstances') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.reloadInstances(data.payload.instances);
|
||||
}
|
||||
} else if (data.command === 'addInstances') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.addInstances(data.payload.instances);
|
||||
inGameEditor.setSelectedObjects(
|
||||
data.payload.instances.map((instance) => instance.persistentUuid)
|
||||
);
|
||||
if (data.payload.moveUnderCursor) {
|
||||
inGameEditor.moveSelectionUnderCursor();
|
||||
}
|
||||
}
|
||||
} else if (data.command === 'deleteSelection') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.deleteSelection();
|
||||
}
|
||||
} else if (data.command === 'dragNewInstance') {
|
||||
const gameCoords = runtimeGame
|
||||
.getRenderer()
|
||||
.convertPageToGameCoords(data.x, data.y);
|
||||
runtimeGame
|
||||
.getInputManager()
|
||||
.onMouseMove(gameCoords[0], gameCoords[1]);
|
||||
|
||||
if (inGameEditor)
|
||||
inGameEditor.dragNewInstance({
|
||||
name: data.name,
|
||||
dropped: data.dropped,
|
||||
isAltPressed: data.isAltPressed,
|
||||
});
|
||||
} else if (data.command === 'cancelDragNewInstance') {
|
||||
if (inGameEditor) inGameEditor.cancelDragNewInstance();
|
||||
} else if (data.command === 'setInstancesEditorSettings') {
|
||||
if (inGameEditor)
|
||||
inGameEditor.updateInstancesEditorSettings(
|
||||
data.payload.instancesEditorSettings
|
||||
);
|
||||
} else if (data.command === 'zoomToInitialPosition') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.zoomToInitialPosition(data.payload.visibleScreenArea);
|
||||
}
|
||||
} else if (data.command === 'zoomToFitContent') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.zoomToFitContent(data.payload.visibleScreenArea);
|
||||
}
|
||||
} else if (data.command === 'setSelectedLayer') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.setSelectedLayerName(data.payload.layerName);
|
||||
}
|
||||
} else if (data.command === 'zoomToFitSelection') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.zoomToFitSelection(data.payload.visibleScreenArea);
|
||||
}
|
||||
} else if (data.command === 'zoomBy') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.zoomBy(data.payload.zoomFactor);
|
||||
}
|
||||
} else if (data.command === 'setZoom') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.setZoom(data.payload.zoom);
|
||||
}
|
||||
} else if (data.command === 'setSelectedInstances') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.setSelectedObjects(data.payload.instanceUuids);
|
||||
}
|
||||
} else if (data.command === 'centerViewOnLastSelectedInstance') {
|
||||
if (inGameEditor) {
|
||||
// TODO: use data.payload.visibleScreenArea
|
||||
inGameEditor.centerViewOnLastSelectedInstance();
|
||||
}
|
||||
} else if (data.command === 'updateInnerArea') {
|
||||
if (inGameEditor) {
|
||||
inGameEditor.updateInnerArea(
|
||||
data.payload.areaMinX,
|
||||
data.payload.areaMinY,
|
||||
data.payload.areaMinZ,
|
||||
data.payload.areaMaxX,
|
||||
data.payload.areaMaxY,
|
||||
data.payload.areaMaxZ
|
||||
);
|
||||
}
|
||||
} else if (data.command === 'getSelectionAABB') {
|
||||
if (inGameEditor) {
|
||||
this.sendSelectionAABB(data.messageId);
|
||||
}
|
||||
} else if (data.command === 'hardReload') {
|
||||
// This usually means that the preview was modified so much that an entire reload
|
||||
// is needed, or that the runtime itself could have been modified.
|
||||
this.launchHardReload();
|
||||
} else {
|
||||
logger.info(
|
||||
'Unknown command "' + data.command + '" received by the debugger.'
|
||||
);
|
||||
that.sendProfilerStopped();
|
||||
});
|
||||
that.sendProfilerStarted();
|
||||
} else if (data.command === 'profiler.stop') {
|
||||
runtimeGame.stopCurrentSceneProfiler();
|
||||
} else if (data.command === 'hotReload') {
|
||||
that._hotReloader.hotReload().then((logs) => {
|
||||
that.sendHotReloaderLogs(logs);
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
'Unknown command "' + data.command + '" received by the debugger.'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.onUncaughtException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,9 +543,12 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
onUncaughtException(exception: Error): void {
|
||||
logger.error('Uncaught exception: ' + exception);
|
||||
logger.error('Uncaught exception: ', exception, exception.stack);
|
||||
|
||||
this._inGameDebugger.setUncaughtException(exception);
|
||||
const runtimeGame = this._runtimegame;
|
||||
if (!runtimeGame.isInGameEdition()) {
|
||||
this._inGameDebugger.setUncaughtException(exception);
|
||||
}
|
||||
|
||||
if (!this._hasLoggedUncaughtException) {
|
||||
// Only log an uncaught exception once, to avoid spamming the debugger server
|
||||
@@ -435,6 +651,20 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
sendRuntimeGameStatus(): void {
|
||||
const currentScene = this._runtimegame.getSceneStack().getCurrentScene();
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'status',
|
||||
payload: {
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: currentScene ? currentScene.getName() : null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
|
||||
*/
|
||||
@@ -515,7 +745,10 @@ namespace gdjs {
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'hotReloader.logs',
|
||||
payload: logs,
|
||||
payload: {
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
logs,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -544,26 +777,152 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is paused.
|
||||
*/
|
||||
sendGamePaused(): void {
|
||||
sendInstanceChanges(changes: {
|
||||
isSendingBackSelectionForDefaultSize: boolean;
|
||||
updatedInstances: Array<InstanceData>;
|
||||
addedInstances: Array<InstanceData>;
|
||||
selectedInstances: Array<InstancePersistentUuidData>;
|
||||
removedInstances: Array<InstancePersistentUuidData>;
|
||||
}): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.paused',
|
||||
payload: null,
|
||||
command: 'updateInstances',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: changes,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is resumed.
|
||||
*/
|
||||
sendGameResumed(): void {
|
||||
sendOpenContextMenu(cursorX: float, cursorY: float): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.resumed',
|
||||
payload: null,
|
||||
command: 'openContextMenu',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: { cursorX, cursorY },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendCameraState(cameraState: EditorCameraState): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'setCameraState',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: cameraState,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendUndo(): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'undo',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendRedo(): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'redo',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendCopy(): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'copy',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendPaste(): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'paste',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendCut(): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'cut',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
payload: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sendSelectionAABB(messageId: number): void {
|
||||
const inGameEditor = this._runtimegame.getInGameEditor();
|
||||
if (!inGameEditor) {
|
||||
return;
|
||||
}
|
||||
const selectionAABB = inGameEditor.getSelectionAABB();
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'selectionAABB',
|
||||
editorId: inGameEditor.getEditorId(),
|
||||
messageId,
|
||||
payload: selectionAABB
|
||||
? {
|
||||
minX: selectionAABB.min[0],
|
||||
minY: selectionAABB.min[1],
|
||||
minZ: selectionAABB.min[2],
|
||||
maxX: selectionAABB.max[0],
|
||||
maxY: selectionAABB.max[1],
|
||||
maxZ: selectionAABB.max[2],
|
||||
}
|
||||
: {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
minZ: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
maxZ: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -587,5 +946,43 @@ namespace gdjs {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
launchHardReload(): void {
|
||||
try {
|
||||
const reloadUrl = new URL(location.href);
|
||||
|
||||
// Construct the initial status to be restored.
|
||||
const initialRuntimeGameStatus =
|
||||
this._runtimegame.getAdditionalOptions().initialRuntimeGameStatus;
|
||||
// We use empty strings to avoid `null` to become `"null"`.
|
||||
const runtimeGameStatus: RuntimeGameStatus = {
|
||||
editorId: initialRuntimeGameStatus?.editorId || '',
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: initialRuntimeGameStatus?.sceneName || '',
|
||||
injectedExternalLayoutName:
|
||||
initialRuntimeGameStatus?.injectedExternalLayoutName || '',
|
||||
skipCreatingInstancesFromScene:
|
||||
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
|
||||
eventsBasedObjectType:
|
||||
initialRuntimeGameStatus?.eventsBasedObjectType || '',
|
||||
eventsBasedObjectVariantName:
|
||||
initialRuntimeGameStatus?.eventsBasedObjectVariantName || '',
|
||||
editorCamera3D: this._runtimegame.getInGameEditor()?.getCameraState(),
|
||||
};
|
||||
|
||||
reloadUrl.searchParams.set(
|
||||
'runtimeGameStatus',
|
||||
JSON.stringify(runtimeGameStatus)
|
||||
);
|
||||
location.replace(reloadUrl);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Could not reload the game with the new initial status',
|
||||
error
|
||||
);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -144,18 +144,30 @@ namespace gdjs {
|
||||
});
|
||||
}
|
||||
|
||||
hotReload(): Promise<HotReloaderLog[]> {
|
||||
async hotReload({
|
||||
shouldReloadResources,
|
||||
projectData: newProjectData,
|
||||
runtimeGameOptions: newRuntimeGameOptions,
|
||||
}: {
|
||||
shouldReloadResources: boolean;
|
||||
projectData: ProjectData;
|
||||
runtimeGameOptions: RuntimeGameOptions;
|
||||
}): Promise<HotReloaderLog[]> {
|
||||
logger.info('Hot reload started');
|
||||
const wasPaused = this._runtimeGame.isPaused();
|
||||
this._runtimeGame.pause(true);
|
||||
this._logs = [];
|
||||
|
||||
// Save old data of the project, to be used to compute
|
||||
// the difference between the old and new project data:
|
||||
|
||||
const oldProjectData: ProjectData = gdjs.projectData;
|
||||
gdjs.projectData = newProjectData;
|
||||
|
||||
const oldScriptFiles = gdjs.runtimeGameOptions
|
||||
.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const oldRuntimeGameOptions = gdjs.runtimeGameOptions;
|
||||
gdjs.runtimeGameOptions = newRuntimeGameOptions;
|
||||
|
||||
const oldScriptFiles =
|
||||
oldRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
|
||||
oldScriptFiles.forEach((scriptFile) => {
|
||||
this._alreadyLoadedScriptFiles[scriptFile.path] = true;
|
||||
@@ -167,76 +179,102 @@ namespace gdjs {
|
||||
gdjs.behaviorsTypes.items[behaviorTypeName];
|
||||
}
|
||||
|
||||
// Reload projectData and runtimeGameOptions stored by convention in data.js:
|
||||
return this._reloadScript('data.js').then(() => {
|
||||
const newProjectData: ProjectData = gdjs.projectData;
|
||||
if (gdjs.inAppTutorialMessage) {
|
||||
gdjs.inAppTutorialMessage.displayInAppTutorialMessage(
|
||||
this._runtimeGame,
|
||||
newRuntimeGameOptions.inAppTutorialMessageInPreview,
|
||||
newRuntimeGameOptions.inAppTutorialMessagePositionInPreview || ''
|
||||
);
|
||||
}
|
||||
|
||||
const newRuntimeGameOptions: RuntimeGameOptions =
|
||||
gdjs.runtimeGameOptions;
|
||||
const newScriptFiles =
|
||||
newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const shouldGenerateScenesEventsCode =
|
||||
!!newRuntimeGameOptions.shouldGenerateScenesEventsCode;
|
||||
const shouldReloadLibraries =
|
||||
!!newRuntimeGameOptions.shouldReloadLibraries;
|
||||
|
||||
if (gdjs.inAppTutorialMessage) {
|
||||
gdjs.inAppTutorialMessage.displayInAppTutorialMessage(
|
||||
this._runtimeGame,
|
||||
newRuntimeGameOptions.inAppTutorialMessageInPreview,
|
||||
newRuntimeGameOptions.inAppTutorialMessagePositionInPreview || ''
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
try {
|
||||
if (shouldReloadLibraries) {
|
||||
await this.reloadScriptFiles(
|
||||
newProjectData,
|
||||
oldScriptFiles,
|
||||
newScriptFiles,
|
||||
shouldGenerateScenesEventsCode
|
||||
);
|
||||
}
|
||||
|
||||
const newScriptFiles =
|
||||
newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const projectDataOnlyExport =
|
||||
!!newRuntimeGameOptions.projectDataOnlyExport;
|
||||
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
return this.reloadScriptFiles(
|
||||
newProjectData,
|
||||
oldScriptFiles,
|
||||
newScriptFiles,
|
||||
projectDataOnlyExport
|
||||
)
|
||||
.then(() => {
|
||||
const changedRuntimeBehaviors =
|
||||
this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
return this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
const newRuntimeGameStatus =
|
||||
newRuntimeGameOptions.initialRuntimeGameStatus;
|
||||
if (
|
||||
newRuntimeGameStatus &&
|
||||
newRuntimeGameStatus.editorId &&
|
||||
newRuntimeGameStatus.isInGameEdition
|
||||
) {
|
||||
if (shouldReloadResources) {
|
||||
// Unloading all resources will force them to be loaded again,
|
||||
// which is sufficient for ensuring they are up-to-date as
|
||||
// resources will be loaded with a 'cache bursting' parameter.
|
||||
this._runtimeGame._resourcesLoader.unloadAllResources();
|
||||
}
|
||||
// The editor don't need to hot-reload the current scene because the
|
||||
// editor always stays in the initial state.
|
||||
this._runtimeGame.setProjectData(newProjectData);
|
||||
await this._runtimeGame.loadFirstAssetsAndStartBackgroundLoading(
|
||||
newRuntimeGameStatus.sceneName || newProjectData.firstLayout,
|
||||
() => {}
|
||||
);
|
||||
const inGameEditor = this._runtimeGame.getInGameEditor();
|
||||
if (inGameEditor) {
|
||||
await inGameEditor.switchToSceneOrVariant(
|
||||
newRuntimeGameStatus.editorId || null,
|
||||
newRuntimeGameStatus.sceneName,
|
||||
newRuntimeGameStatus.injectedExternalLayoutName,
|
||||
newRuntimeGameStatus.eventsBasedObjectType,
|
||||
newRuntimeGameStatus.eventsBasedObjectVariantName,
|
||||
newRuntimeGameStatus.editorCamera3D || null
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message +
|
||||
'\n' +
|
||||
error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(false);
|
||||
return this._logs;
|
||||
}
|
||||
} else {
|
||||
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
await this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message +
|
||||
'\n' +
|
||||
error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(wasPaused);
|
||||
return this._logs;
|
||||
}
|
||||
|
||||
_computeChangedRuntimeBehaviors(
|
||||
@@ -281,12 +319,12 @@ namespace gdjs {
|
||||
newProjectData: ProjectData,
|
||||
oldScriptFiles: RuntimeGameOptionsScriptFile[],
|
||||
newScriptFiles: RuntimeGameOptionsScriptFile[],
|
||||
projectDataOnlyExport: boolean
|
||||
shouldGenerateScenesEventsCode: boolean
|
||||
): Promise<void[]> {
|
||||
const reloadPromises: Array<Promise<void>> = [];
|
||||
|
||||
// Reload events, only if they were exported.
|
||||
if (!projectDataOnlyExport) {
|
||||
if (shouldGenerateScenesEventsCode) {
|
||||
newProjectData.layouts.forEach((_layoutData, index) => {
|
||||
reloadPromises.push(this._reloadScript('code' + index + '.js'));
|
||||
});
|
||||
@@ -326,7 +364,7 @@ namespace gdjs {
|
||||
)[0];
|
||||
|
||||
// A file may be removed because of a partial preview.
|
||||
if (!newScriptFile && !projectDataOnlyExport) {
|
||||
if (!newScriptFile && !shouldGenerateScenesEventsCode) {
|
||||
this._logs.push({
|
||||
kind: 'warning',
|
||||
message: 'Script file ' + oldScriptFile.path + ' was removed.',
|
||||
@@ -694,6 +732,16 @@ namespace gdjs {
|
||||
runtimeScene.setEventsGeneratedCodeFunction(newLayoutData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the children object data into every custom object data.
|
||||
*
|
||||
* At the runtime, this is done at the object instantiation.
|
||||
* For hot-reloading, it's done before hands to optimize.
|
||||
*
|
||||
* @param projectData The project data
|
||||
* @param objectDatas The object datas to modify
|
||||
* @returns
|
||||
*/
|
||||
static resolveCustomObjectConfigurations(
|
||||
projectData: ProjectData,
|
||||
objectDatas: ObjectData[]
|
||||
@@ -717,27 +765,43 @@ namespace gdjs {
|
||||
if (!eventsBasedObjectData) {
|
||||
return objectData;
|
||||
}
|
||||
|
||||
const customObjectConfiguration = objectData as ObjectData &
|
||||
CustomObjectConfiguration;
|
||||
const eventsBasedObjectVariantData =
|
||||
gdjs.RuntimeGame._getEventsBasedObjectVariantData(
|
||||
eventsBasedObjectData,
|
||||
customObjectConfiguration.variant
|
||||
);
|
||||
|
||||
// Apply the legacy children configuration overriding if any.
|
||||
const mergedChildObjectDataList =
|
||||
customObjectConfiguration.childrenContent
|
||||
? eventsBasedObjectData.objects.map((objectData) => ({
|
||||
...objectData,
|
||||
...customObjectConfiguration.childrenContent[objectData.name],
|
||||
}))
|
||||
gdjs.CustomRuntimeObjectInstanceContainer.hasChildrenConfigurationOverriding(
|
||||
customObjectConfiguration,
|
||||
eventsBasedObjectVariantData
|
||||
)
|
||||
? eventsBasedObjectData.objects.map((objectData) =>
|
||||
customObjectConfiguration.childrenContent
|
||||
? {
|
||||
...objectData,
|
||||
...customObjectConfiguration.childrenContent[
|
||||
objectData.name
|
||||
],
|
||||
}
|
||||
: objectData
|
||||
)
|
||||
: eventsBasedObjectData.objects;
|
||||
|
||||
const mergedObjectConfiguration = {
|
||||
...eventsBasedObjectData,
|
||||
...objectData,
|
||||
// ObjectData doesn't have an `objects` attribute.
|
||||
// ObjectData doesn't have an `objects` nor `instances` attribute.
|
||||
// This is a small optimization to avoid to create an
|
||||
// InstanceContainerData for each instance to hot-reload their inner
|
||||
// scene (see `_hotReloadRuntimeInstanceContainer` call from
|
||||
// `_hotReloadRuntimeSceneInstances`).
|
||||
...eventsBasedObjectData,
|
||||
...eventsBasedObjectVariantData,
|
||||
objects: mergedChildObjectDataList,
|
||||
// It must be the last one to ensure the object name won't be overridden.
|
||||
...objectData,
|
||||
};
|
||||
return mergedObjectConfiguration;
|
||||
});
|
||||
@@ -751,6 +815,12 @@ namespace gdjs {
|
||||
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
if (!oldLayoutData.objects || !newLayoutData.objects) {
|
||||
// It can happen when `hotReloadRuntimeInstances` is executed.
|
||||
// `hotReloadRuntimeInstances` doesn't resolve the custom objects
|
||||
// because it can only modify the 1st level of instances.
|
||||
return;
|
||||
}
|
||||
const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations(
|
||||
oldProjectData,
|
||||
oldLayoutData.objects
|
||||
@@ -921,16 +991,62 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
hotReloadRuntimeSceneObjects(
|
||||
updatedObjects: Array<ObjectData>,
|
||||
// runtimeInstanceContainer gives an access as a map.
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
const oldObjects: Array<ObjectData | null> = updatedObjects.map(
|
||||
(objectData) =>
|
||||
runtimeInstanceContainer._objects.get(objectData.name) || null
|
||||
);
|
||||
|
||||
const projectData: ProjectData = this._runtimeGame._data;
|
||||
const newObjectDataList = HotReloader.resolveCustomObjectConfigurations(
|
||||
projectData,
|
||||
updatedObjects
|
||||
);
|
||||
|
||||
this._hotReloadRuntimeSceneObjects(
|
||||
oldObjects,
|
||||
newObjectDataList,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
// Update the GameData
|
||||
for (let index = 0; index < updatedObjects.length; index++) {
|
||||
const oldObjectData = oldObjects[index];
|
||||
// When the object is new, the hot-reload call `registerObject`
|
||||
// so `_objects` is already updated.
|
||||
if (oldObjectData) {
|
||||
// In gdjs.CustomRuntimeObjectInstanceContainer.loadFrom, object can
|
||||
// be registered with a different instance from the ProjectData. This
|
||||
// is only done for children of a custom object with a children overriding.
|
||||
// In the case of the editor, the fake custom object used for editing
|
||||
// variants has no children overriding (see
|
||||
// gdjs.RuntimeGame._createSceneWithCustomObject).
|
||||
// Thus, the oldObjectData is always the one from the ProjectData.
|
||||
HotReloader.assignOrDelete(oldObjectData, updatedObjects[index]);
|
||||
} else {
|
||||
console.warn(
|
||||
`Can't update object data for "${updatedObjects[index].name}" because it doesn't exist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hotReloadRuntimeSceneObjects(
|
||||
oldObjects: ObjectData[],
|
||||
oldObjects: Array<ObjectData | null>,
|
||||
newObjects: ObjectData[],
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
oldObjects.forEach((oldObjectData) => {
|
||||
if (!oldObjectData) {
|
||||
return;
|
||||
}
|
||||
const name = oldObjectData.name;
|
||||
const newObjectData = newObjects.filter(
|
||||
const newObjectData = newObjects.find(
|
||||
(objectData) => objectData.name === name
|
||||
)[0];
|
||||
);
|
||||
|
||||
// Note: if an object is renamed in the editor, it will be considered as removed,
|
||||
// and the new object name as a new object to register.
|
||||
@@ -952,9 +1068,9 @@ namespace gdjs {
|
||||
});
|
||||
newObjects.forEach((newObjectData) => {
|
||||
const name = newObjectData.name;
|
||||
const oldObjectData = oldObjects.filter(
|
||||
(layerData) => layerData.name === name
|
||||
)[0];
|
||||
const oldObjectData = oldObjects.find(
|
||||
(layerData) => layerData && layerData.name === name
|
||||
);
|
||||
if (
|
||||
(!oldObjectData || oldObjectData.type !== newObjectData.type) &&
|
||||
!runtimeInstanceContainer.isObjectRegistered(name)
|
||||
@@ -1192,6 +1308,31 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
hotReloadRuntimeSceneLayers(
|
||||
newLayers: LayerData[],
|
||||
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
const layerNames = [];
|
||||
runtimeInstanceContainer.getAllLayerNames(layerNames);
|
||||
const oldLayers = layerNames.map((layerName) =>
|
||||
runtimeInstanceContainer.hasLayer(layerName)
|
||||
? runtimeInstanceContainer.getLayer(layerName)._initialLayerData
|
||||
: null
|
||||
);
|
||||
this._hotReloadRuntimeSceneLayers(
|
||||
oldLayers.filter(Boolean) as LayerData[],
|
||||
newLayers,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
// Update the GameData
|
||||
for (let index = 0; index < newLayers.length; index++) {
|
||||
const oldLayer = oldLayers[index];
|
||||
if (oldLayer) {
|
||||
HotReloader.assignOrDelete(oldLayer, newLayers[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hotReloadRuntimeSceneLayers(
|
||||
oldLayers: LayerData[],
|
||||
newLayers: LayerData[],
|
||||
@@ -1273,6 +1414,8 @@ namespace gdjs {
|
||||
newLayer.effects,
|
||||
runtimeLayer
|
||||
);
|
||||
|
||||
runtimeLayer._initialLayerData = newLayer;
|
||||
}
|
||||
|
||||
_hotReloadRuntimeLayerEffects(
|
||||
@@ -1357,6 +1500,28 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
hotReloadRuntimeInstances(
|
||||
oldInstances: InstanceData[],
|
||||
newInstances: InstanceData[],
|
||||
runtimeInstanceContainer: RuntimeInstanceContainer
|
||||
): void {
|
||||
const projectData: ProjectData = gdjs.projectData;
|
||||
const objects: Array<ObjectData> = [];
|
||||
runtimeInstanceContainer._objects.values(objects);
|
||||
projectData.layouts;
|
||||
this._hotReloadRuntimeSceneInstances(
|
||||
projectData,
|
||||
projectData,
|
||||
[],
|
||||
objects,
|
||||
objects,
|
||||
oldInstances,
|
||||
newInstances,
|
||||
runtimeInstanceContainer
|
||||
);
|
||||
gdjs.copyArray(newInstances, oldInstances);
|
||||
}
|
||||
|
||||
_hotReloadRuntimeSceneInstances(
|
||||
oldProjectData: ProjectData,
|
||||
newProjectData: ProjectData,
|
||||
@@ -1423,6 +1588,9 @@ namespace gdjs {
|
||||
);
|
||||
} else {
|
||||
// Reload objects that were created at runtime.
|
||||
// This is a subset of what is done by `_hotReloadRuntimeInstance`.
|
||||
// Since the instance doesn't exist in the editor, it's properties
|
||||
// can't be updated, only the object changes are applied.
|
||||
|
||||
// Update variables
|
||||
this._hotReloadVariablesContainer(
|
||||
@@ -1431,6 +1599,7 @@ namespace gdjs {
|
||||
runtimeObject.getVariables()
|
||||
);
|
||||
|
||||
// Update the content of custom object
|
||||
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
|
||||
const childrenInstanceContainer =
|
||||
runtimeObject.getChildrenContainer();
|
||||
@@ -1443,15 +1612,18 @@ namespace gdjs {
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
|
||||
// Reload the content of custom objects that were created at runtime.
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
// Variant swapping is handled by `CustomRuntimeObject.updateFromObjectData`.
|
||||
if (newCustomObjectData.variant === oldCustomObjectData.variant) {
|
||||
// Reload the content of custom objects that were created at runtime.
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1513,22 +1685,16 @@ namespace gdjs {
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(runtimeObject)) {
|
||||
if (oldInstance.z !== newInstance.z && newInstance.z !== undefined) {
|
||||
runtimeObject.setZ(newInstance.z);
|
||||
if (oldInstance.z !== newInstance.z) {
|
||||
runtimeObject.setZ(newInstance.z || 0);
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (
|
||||
oldInstance.rotationX !== newInstance.rotationX &&
|
||||
newInstance.rotationX !== undefined
|
||||
) {
|
||||
runtimeObject.setRotationX(newInstance.rotationX);
|
||||
if (oldInstance.rotationX !== newInstance.rotationX) {
|
||||
runtimeObject.setRotationX(newInstance.rotationX || 0);
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (
|
||||
oldInstance.rotationY !== newInstance.rotationY &&
|
||||
newInstance.rotationY !== undefined
|
||||
) {
|
||||
runtimeObject.setRotationY(newInstance.rotationY);
|
||||
if (oldInstance.rotationY !== newInstance.rotationY) {
|
||||
runtimeObject.setRotationY(newInstance.rotationY || 0);
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -1583,8 +1749,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
|
||||
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
|
||||
|
||||
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
|
||||
const oldCustomObjectData = oldObjectData as ObjectData &
|
||||
CustomObjectConfiguration &
|
||||
@@ -1593,14 +1757,19 @@ namespace gdjs {
|
||||
CustomObjectConfiguration &
|
||||
InstanceContainerData;
|
||||
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
// Variant swapping is handled by `CustomRuntimeObject.updateFromObjectData`.
|
||||
if (newCustomObjectData.variant === oldCustomObjectData.variant) {
|
||||
const childrenInstanceContainer =
|
||||
runtimeObject.getChildrenContainer();
|
||||
this._hotReloadRuntimeInstanceContainer(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
oldCustomObjectData,
|
||||
newCustomObjectData,
|
||||
changedRuntimeBehaviors,
|
||||
childrenInstanceContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update variables
|
||||
@@ -1727,5 +1896,23 @@ namespace gdjs {
|
||||
// true if both NaN, false otherwise
|
||||
return a !== a && b !== b;
|
||||
}
|
||||
|
||||
static assignOrDelete(
|
||||
target: any,
|
||||
source: any,
|
||||
ignoreKeys: string[] = []
|
||||
): void {
|
||||
Object.assign(target, source);
|
||||
for (const key in target) {
|
||||
if (ignoreKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
||||
if (source[key] === undefined) {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -56,6 +56,19 @@ namespace gdjs {
|
||||
};
|
||||
this._ws.onclose = function close() {
|
||||
logger.info('Debugger connection closed');
|
||||
|
||||
if (that._runtimegame.isInGameEdition()) {
|
||||
// Sometimes, for example if the editor is launched for a long time and the device goes to sleep,
|
||||
// the WebSocket connection between the editor and the game is closed. When we are in in-game edition,
|
||||
// we can't afford to lose the connection because it means the editor is unusable.
|
||||
// In this case, we hard reload the game to re-establish a new connection.
|
||||
setTimeout(() => {
|
||||
logger.info(
|
||||
'Debugger connection closed while in in-game edition - this is suspicious so hard reloading to re-establish a new connection.'
|
||||
);
|
||||
that.launchHardReload();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
this._ws.onerror = function errored(error) {
|
||||
logger.warn('Debugger client error:', error);
|
||||
|
@@ -11,7 +11,13 @@ namespace gdjs {
|
||||
constructor(runtimeGame: RuntimeGame) {
|
||||
super(runtimeGame);
|
||||
|
||||
// Opener is either the `opener` for popups, or the `parent` if the game
|
||||
// is running as an iframe (notably: in-game edition).
|
||||
this._opener = window.opener || null;
|
||||
if (!this._opener && window.parent !== window) {
|
||||
this._opener = window.parent;
|
||||
}
|
||||
|
||||
if (!this._opener) {
|
||||
logger.info("`window.opener` not existing, the debugger won't work.");
|
||||
return;
|
||||
|
@@ -207,7 +207,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const resource = this._loadedFontFamily.get(resourceData);
|
||||
const resource = this._loadedFontFamily.getFromName(resourceData.name);
|
||||
if (resource) {
|
||||
this._loadedFontFamily.delete(resourceData);
|
||||
}
|
||||
|
@@ -1157,12 +1157,12 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const musicRes = this._loadedMusics.get(resourceData);
|
||||
const musicRes = this._loadedMusics.getFromName(resourceData.name);
|
||||
if (musicRes) {
|
||||
this.unloadAudio(resourceData.name, true);
|
||||
}
|
||||
|
||||
const soundRes = this._loadedSounds.get(resourceData);
|
||||
const soundRes = this._loadedSounds.getFromName(resourceData.name);
|
||||
if (soundRes) {
|
||||
this.unloadAudio(resourceData.name, false);
|
||||
}
|
||||
|
@@ -33,24 +33,28 @@ namespace gdjs {
|
||||
/**
|
||||
* The cursor X position (moved by mouse and touch events).
|
||||
*/
|
||||
_cursorX: float = 0;
|
||||
private _cursorX: float = 0;
|
||||
/**
|
||||
* The cursor Y position (moved by mouse and touch events).
|
||||
*/
|
||||
_cursorY: float = 0;
|
||||
private _cursorY: float = 0;
|
||||
/**
|
||||
* The mouse X position (only moved by mouse events).
|
||||
*/
|
||||
_mouseX: float = 0;
|
||||
private _mouseX: float = 0;
|
||||
/**
|
||||
* The mouse Y position (only moved by mouse events).
|
||||
*/
|
||||
_mouseY: float = 0;
|
||||
_isMouseInsideCanvas: boolean = true;
|
||||
_mouseWheelDelta: float = 0;
|
||||
private _mouseY: float = 0;
|
||||
private _isMouseInsideCanvas: boolean = true;
|
||||
private _wheelDeltaX: float = 0;
|
||||
private _wheelDeltaY: float = 0;
|
||||
private _wheelDeltaZ: float = 0;
|
||||
|
||||
// TODO Remove _touches when there is no longer SpritePanelButton 1.2.0
|
||||
// extension in the wild.
|
||||
_touches = {
|
||||
// @ts-ignore
|
||||
private _touches = {
|
||||
firstKey: (): string | number | null => {
|
||||
for (const key in this._mouseOrTouches.items) {
|
||||
// Exclude mouse key.
|
||||
@@ -61,22 +65,23 @@ namespace gdjs {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
_mouseOrTouches: Hashtable<Touch>;
|
||||
|
||||
private _mouseOrTouches: Hashtable<Touch>;
|
||||
//Identifiers of the touches that started during/before the frame.
|
||||
_startedTouches: Array<integer> = [];
|
||||
private _startedTouches: Array<integer> = [];
|
||||
|
||||
//Identifiers of the touches that ended during/before the frame.
|
||||
_endedTouches: Array<integer> = [];
|
||||
_touchSimulateMouse: boolean = true;
|
||||
private _endedTouches: Array<integer> = [];
|
||||
private _touchSimulateMouse: boolean = true;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
_lastStartedTouchIndex = 0;
|
||||
private _lastStartedTouchIndex = 0;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
_lastEndedTouchIndex = 0;
|
||||
private _lastEndedTouchIndex = 0;
|
||||
|
||||
constructor() {
|
||||
this._pressedKeys = new Hashtable();
|
||||
@@ -96,7 +101,7 @@ namespace gdjs {
|
||||
* @param keyCode The raw key code
|
||||
* @param location The location
|
||||
*/
|
||||
_getLocationAwareKeyCode(
|
||||
static getLocationAwareKeyCode(
|
||||
keyCode: number,
|
||||
location: number | null | undefined
|
||||
): integer {
|
||||
@@ -121,7 +126,7 @@ namespace gdjs {
|
||||
* @param location The location of the event.
|
||||
*/
|
||||
onKeyPressed(keyCode: number, location?: number): void {
|
||||
const locationAwareKeyCode = this._getLocationAwareKeyCode(
|
||||
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
|
||||
keyCode,
|
||||
location
|
||||
);
|
||||
@@ -138,7 +143,7 @@ namespace gdjs {
|
||||
* @param location The location of the event.
|
||||
*/
|
||||
onKeyReleased(keyCode: number, location?: number): void {
|
||||
const locationAwareKeyCode = this._getLocationAwareKeyCode(
|
||||
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
|
||||
keyCode,
|
||||
location
|
||||
);
|
||||
@@ -335,6 +340,19 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if any mouse button is pressed.
|
||||
* @return true if any mouse button is pressed.
|
||||
*/
|
||||
anyMouseButtonPressed(): boolean {
|
||||
for (const buttonCode in this._pressedMouseButtons) {
|
||||
if (this._pressedMouseButtons[buttonCode]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_setMouseButtonPressed(buttonCode: number): void {
|
||||
this._pressedMouseButtons[buttonCode] = true;
|
||||
this._releasedMouseButtons[buttonCode] = false;
|
||||
@@ -380,17 +398,37 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Should be called whenever the mouse wheel is used
|
||||
* @param wheelDelta The mouse wheel delta
|
||||
* @param wheelDeltaY The mouse wheel delta
|
||||
*/
|
||||
onMouseWheel(wheelDelta: number): void {
|
||||
this._mouseWheelDelta = wheelDelta;
|
||||
onMouseWheel(
|
||||
wheelDeltaY: number,
|
||||
wheelDeltaX: number,
|
||||
wheelDeltaZ: number
|
||||
): void {
|
||||
this._wheelDeltaY = wheelDeltaY;
|
||||
if (wheelDeltaX !== undefined) this._wheelDeltaX = wheelDeltaX;
|
||||
if (wheelDeltaZ !== undefined) this._wheelDeltaZ = wheelDeltaZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mouse wheel delta
|
||||
* Return the mouse wheel delta on Y axis.
|
||||
*/
|
||||
getMouseWheelDelta(): float {
|
||||
return this._mouseWheelDelta;
|
||||
return this._wheelDeltaY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mouse wheel delta on X axis.
|
||||
*/
|
||||
getMouseWheelDeltaX(): float {
|
||||
return this._wheelDeltaX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mouse wheel delta on Z axis.
|
||||
*/
|
||||
getMouseWheelDeltaZ(): float {
|
||||
return this._wheelDeltaZ;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,7 +616,9 @@ namespace gdjs {
|
||||
this._releasedKeys.clear();
|
||||
this._justPressedKeys.clear();
|
||||
this._releasedMouseButtons.length = 0;
|
||||
this._mouseWheelDelta = 0;
|
||||
this._wheelDeltaX = 0;
|
||||
this._wheelDeltaY = 0;
|
||||
this._wheelDeltaZ = 0;
|
||||
this._lastStartedTouchIndex = 0;
|
||||
this._lastEndedTouchIndex = 0;
|
||||
}
|
||||
|
@@ -210,12 +210,12 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedJson = this._loadedJsons.get(resourceData);
|
||||
const loadedJson = this._loadedJsons.getFromName(resourceData.name);
|
||||
if (loadedJson) {
|
||||
this._loadedJsons.delete(resourceData);
|
||||
}
|
||||
|
||||
const callback = this._callbacks.get(resourceData);
|
||||
const callback = this._callbacks.getFromName(resourceData.name);
|
||||
if (callback) {
|
||||
this._callbacks.delete(resourceData);
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -7,6 +7,178 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('LayerPixiRenderer');
|
||||
|
||||
const FRUSTUM_EDGES: Array<[number, number]> = [
|
||||
// near plane edges
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 0],
|
||||
// far plane edges
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[7, 4],
|
||||
// near↔far connections
|
||||
[0, 4],
|
||||
[1, 5],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
];
|
||||
|
||||
/** Normalized Device Coordinates corners for near (-1) and far (+1) planes (Three.js NDC: z=-1 near, z=+1 far). */
|
||||
const NDC_CORNERS: Array<Array<float>> = [
|
||||
// near
|
||||
[-1, -1, -1],
|
||||
[+1, -1, -1],
|
||||
[+1, +1, -1],
|
||||
[-1, +1, -1],
|
||||
// far
|
||||
[-1, -1, +1],
|
||||
[+1, -1, +1],
|
||||
[+1, +1, +1],
|
||||
[-1, +1, +1],
|
||||
];
|
||||
|
||||
/** Sort convex polygon vertices around centroid to get consistent winding. */
|
||||
const sortConvexPolygon = (points: THREE.Vector3[]): THREE.Vector3[] => {
|
||||
if (points.length <= 2) return points;
|
||||
const cx = points.reduce((s, p) => s + p.x, 0) / points.length;
|
||||
const cy = points.reduce((s, p) => s + p.y, 0) / points.length;
|
||||
return points
|
||||
.map((p) => ({ p, a: Math.atan2(p.y - cy, p.x - cx) }))
|
||||
.sort((u, v) => u.a - v.a)
|
||||
.map((u) => u.p);
|
||||
};
|
||||
|
||||
/**
|
||||
* Intersect a frustum edge segment [a,b] with plane Z=0.
|
||||
* Returns point or null if no intersection on the segment.
|
||||
*/
|
||||
const intersectSegmentWithZ0 = (
|
||||
a: THREE.Vector3,
|
||||
b: THREE.Vector3,
|
||||
eps = 1e-9
|
||||
): THREE.Vector3 | null => {
|
||||
const az = a.z,
|
||||
bz = b.z;
|
||||
const dz = bz - az;
|
||||
|
||||
// If both z on same side and not on plane, no crossing.
|
||||
if (Math.abs(dz) < eps) {
|
||||
// Segment is (almost) parallel to plane.
|
||||
if (Math.abs(az) < eps && Math.abs(bz) < eps) {
|
||||
// Entire segment lies on plane: return endpoints (handled by caller via dedup).
|
||||
// Here we return null and let caller add endpoints if needed.
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Solve a.z + t*(b.z - a.z) = 0 ⇒ t = -a.z / (b.z - a.z)
|
||||
const t = -az / dz;
|
||||
if (t < -eps || t > 1 + eps) {
|
||||
// Intersection beyond the segment bounds.
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = new THREE.Vector3(
|
||||
a.x + t * (b.x - a.x),
|
||||
a.y + t * (b.y - a.y),
|
||||
0
|
||||
);
|
||||
return p;
|
||||
};
|
||||
|
||||
/** Remove near-duplicate points. */
|
||||
const dedupPoints = (
|
||||
points: THREE.Vector3[],
|
||||
eps = 1e-6
|
||||
): THREE.Vector3[] => {
|
||||
const out: THREE.Vector3[] = [];
|
||||
for (const p of points) {
|
||||
const exists = out.some(
|
||||
(q) => Math.abs(p.x - q.x) < eps && Math.abs(p.y - q.y) < eps
|
||||
);
|
||||
if (!exists) out.push(p);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the convex polygon of the camera frustum clipped by plane Z=0.
|
||||
* Returns ordered vertices (world coords, z=0). Empty array if no intersection.
|
||||
*/
|
||||
const clipFrustumAgainstZ0 = (camera: THREE.Camera): THREE.Vector3[] => {
|
||||
camera.updateMatrixWorld(true);
|
||||
|
||||
// Get the 8 corners of the camera frustum in world coordinates.
|
||||
const corners = NDC_CORNERS.map((ndc) =>
|
||||
new THREE.Vector3(ndc[0], ndc[1], ndc[2]).unproject(camera)
|
||||
);
|
||||
if (corners.length !== 8) return [];
|
||||
|
||||
const hits: THREE.Vector3[] = [];
|
||||
|
||||
// 1) Add vertices that already lie on the plane (z≈0).
|
||||
for (const v of corners) {
|
||||
if (Math.abs(v.z) < 1e-9) {
|
||||
hits.push(new THREE.Vector3(v.x, v.y, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Intersect each frustum edge with plane Z=0.
|
||||
for (const [i, j] of FRUSTUM_EDGES) {
|
||||
const a = corners[i],
|
||||
b = corners[j];
|
||||
const p = intersectSegmentWithZ0(a, b);
|
||||
if (p) hits.push(p);
|
||||
}
|
||||
|
||||
// Deduplicate and order.
|
||||
const unique = dedupPoints(hits);
|
||||
if (unique.length < 3) return [];
|
||||
return sortConvexPolygon(unique);
|
||||
};
|
||||
|
||||
/**
|
||||
* Intersect the ray going through a normalized device coordinate (nx, ny)
|
||||
* with the plane Z=0. Returns the hit point in THREE world coords (z=0)
|
||||
* or null if the ray doesn't intersect the plane in front of the camera.
|
||||
*/
|
||||
const projectNDCToZ0 = (
|
||||
camera: THREE.Camera,
|
||||
nx: number,
|
||||
ny: number
|
||||
): THREE.Vector3 | null => {
|
||||
if (!camera) return null;
|
||||
|
||||
camera.updateMatrixWorld(true);
|
||||
|
||||
const origin = new THREE.Vector3();
|
||||
const dir = new THREE.Vector3();
|
||||
const p = new THREE.Vector3(nx, ny, 0.5);
|
||||
|
||||
if (camera instanceof THREE.OrthographicCamera) {
|
||||
// For ortho, unproject a point on the camera plane, and use forward dir.
|
||||
p.z = 0; // on the camera plane
|
||||
p.unproject(camera); // gives a point on the camera plane in world coords
|
||||
origin.copy(p);
|
||||
camera.getWorldDirection(dir);
|
||||
} else {
|
||||
// Perspective: unproject a point on the frustum plane, build a ray.
|
||||
p.unproject(camera);
|
||||
origin.copy(camera.position);
|
||||
dir.copy(p).sub(origin).normalize();
|
||||
}
|
||||
|
||||
const dz = dir.z;
|
||||
if (Math.abs(dz) < 1e-8) return null; // parallel
|
||||
const t = -origin.z / dz;
|
||||
if (t <= 0) return null; // behind the camera => not visible
|
||||
|
||||
return origin.addScaledVector(dir, t).setZ(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* The renderer for a gdjs.Layer using Pixi.js.
|
||||
*/
|
||||
@@ -47,6 +219,7 @@ namespace gdjs {
|
||||
private _threePlaneGeometry: THREE.PlaneGeometry | null = null;
|
||||
private _threePlaneMaterial: THREE.ShaderMaterial | null = null;
|
||||
private _threePlaneMesh: THREE.Mesh | null = null;
|
||||
private _threePlaneMeshDebugOutline: THREE.LineSegments | null = null;
|
||||
|
||||
/**
|
||||
* Pixi doesn't sort children with zIndex == 0.
|
||||
@@ -99,6 +272,9 @@ namespace gdjs {
|
||||
// The layer is now fully initialized. Adapt the 3D camera position
|
||||
// (which we could not do before in `_setup3DRendering`).
|
||||
this._update3DCameraAspectAndPosition();
|
||||
|
||||
// Uncomment to show the outline of the 2D rendering plane.
|
||||
// this.show2DRenderingPlaneDebugOutline(true);
|
||||
}
|
||||
|
||||
onGameResolutionResized() {
|
||||
@@ -134,6 +310,10 @@ namespace gdjs {
|
||||
return this._threeScene;
|
||||
}
|
||||
|
||||
getThreeGroup(): THREE.Group | null {
|
||||
return this._threeGroup;
|
||||
}
|
||||
|
||||
getThreeCamera():
|
||||
| THREE.PerspectiveCamera
|
||||
| THREE.OrthographicCamera
|
||||
@@ -285,6 +465,10 @@ namespace gdjs {
|
||||
'Tried to setup PixiJS plane for 2D rendering in 3D for a layer that is already set up.'
|
||||
);
|
||||
|
||||
this.set2DPlaneMaxDrawingDistance(
|
||||
this._layer.getInitialCamera2DPlaneMaxDrawingDistance()
|
||||
);
|
||||
|
||||
// If we have both 2D and 3D objects to be rendered, create a render texture that PixiJS will use
|
||||
// to render, and that will be projected on a plane by Three.js
|
||||
this._createPixiRenderTexture(pixiRenderer);
|
||||
@@ -388,30 +572,298 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position of the PIXI container. To be called after each change
|
||||
* made to position, zoom or rotation of the camera.
|
||||
* Enable or disable the drawing of an outline of the 2D rendering plane.
|
||||
* Useful to visually see where the 2D rendering is done in the 3D world.
|
||||
*/
|
||||
updatePosition(): void {
|
||||
const angle = -gdjs.toRad(this._layer.getCameraRotation());
|
||||
const zoomFactor = this._layer.getCameraZoom();
|
||||
this._pixiContainer.rotation = angle;
|
||||
this._pixiContainer.scale.x = zoomFactor;
|
||||
this._pixiContainer.scale.y = zoomFactor;
|
||||
const cosValue = Math.cos(angle);
|
||||
const sinValue = Math.sin(angle);
|
||||
const centerX =
|
||||
this._layer.getCameraX() * zoomFactor * cosValue -
|
||||
this._layer.getCameraY() * zoomFactor * sinValue;
|
||||
const centerY =
|
||||
this._layer.getCameraX() * zoomFactor * sinValue +
|
||||
this._layer.getCameraY() * zoomFactor * cosValue;
|
||||
this._pixiContainer.position.x = this._layer.getWidth() / 2 - centerX;
|
||||
this._pixiContainer.position.y = this._layer.getHeight() / 2 - centerY;
|
||||
show2DRenderingPlaneDebugOutline(enable: boolean) {
|
||||
if (!this._threePlaneMesh) return;
|
||||
if (enable && !this._threePlaneMeshDebugOutline) {
|
||||
// Add rectangle outline around the plane.
|
||||
const edges = new THREE.EdgesGeometry(this._threePlaneGeometry);
|
||||
const lineMaterial = new THREE.LineBasicMaterial({
|
||||
color: 0xff0000,
|
||||
});
|
||||
this._threePlaneMeshDebugOutline = new THREE.LineSegments(
|
||||
edges,
|
||||
lineMaterial
|
||||
);
|
||||
|
||||
// Attach the outline to the plane so it follows position/scale/rotation.
|
||||
this._threePlaneMesh.add(this._threePlaneMeshDebugOutline);
|
||||
}
|
||||
if (!enable && this._threePlaneMeshDebugOutline) {
|
||||
this._threePlaneMesh.remove(this._threePlaneMeshDebugOutline);
|
||||
this._threePlaneMeshDebugOutline = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum size of the 2D plane, in pixels. */
|
||||
private _2DPlaneMaxDrawingDistance: number = 5000;
|
||||
/** Tilt degrees below which the 2D plane is not clamped. */
|
||||
private _2DPlaneClampFreeTiltDeg: number = 0.1;
|
||||
/** Tilt degrees below which the 2D plane is fully clamped. */
|
||||
private _2DPlaneClampHardTiltDeg: number = 6;
|
||||
private _2DPlaneClampRampPower: number = 1.5; // 1 = linear, >1 = smoother
|
||||
|
||||
/**
|
||||
* Set the maximum "drawing distance", in pixels, of the 2D when in the 3D world.
|
||||
* This corresponds to the "height" of the 2D plane.
|
||||
* Used when the 3D camera is tilted on the X or Y axis (instead of looking down the Z axis,
|
||||
* as it's done by default for 2D games).
|
||||
* This is useful to avoid the 2D plane being too big when the camera is tilted.
|
||||
*/
|
||||
set2DPlaneMaxDrawingDistance(h: number) {
|
||||
this._2DPlaneMaxDrawingDistance = Math.max(0, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tilt degrees below which the 2D plane is not clamped.
|
||||
*/
|
||||
set2DPlaneClampFreeTiltDegrees(d: number) {
|
||||
this._2DPlaneClampFreeTiltDeg = Math.max(0, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tilt degrees below which the 2D plane is clamped (see `set2DPlaneMaxDrawingDistance`).
|
||||
*/
|
||||
set2DPlaneClampHardTiltDegrees(d: number) {
|
||||
this._2DPlaneClampHardTiltDeg = Math.max(0, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ramp power of the 2D plane clamping (see `set2DPlaneMaxDrawingDistance`). Used
|
||||
* for smoother transition between clamped and unclamped.
|
||||
*/
|
||||
set2DPlaneClampRampPower(p: number) {
|
||||
this._2DPlaneClampRampPower = Math.max(0.1, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the 2D plane, in the world coordinates.
|
||||
*/
|
||||
private _get2DPlaneSize(): [number, number] {
|
||||
if (!this._threeCamera) return [0, 0];
|
||||
|
||||
// Compute the intersection of the frustrum of the camera on the Z=0 plane.
|
||||
// In theory, that's where the entire 2D rendering should be displayed.
|
||||
const poly = clipFrustumAgainstZ0(this._threeCamera);
|
||||
|
||||
if (poly.length === 0) {
|
||||
// No intersection at all: Z=0 not in view.
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
// Compute the axis-aligned bounds on Z=0 (world units) of the polygon,
|
||||
// so we can compute the size of the plane doing the 2D rendering.
|
||||
let minX = Infinity,
|
||||
maxX = -Infinity,
|
||||
minY = Infinity,
|
||||
maxY = -Infinity;
|
||||
for (const p of poly) {
|
||||
if (p.x < minX) minX = p.x;
|
||||
if (p.x > maxX) maxX = p.x;
|
||||
if (p.y < minY) minY = p.y;
|
||||
if (p.y > maxY) maxY = p.y;
|
||||
}
|
||||
let boxW = Math.max(1e-8, maxX - minX);
|
||||
let boxH = Math.max(1e-8, maxY - minY);
|
||||
|
||||
// Keep 2D layer aspect ratio (so texture isn't stretched).
|
||||
const targetAspect = this._layer.getWidth() / this._layer.getHeight();
|
||||
const boxAspect = boxW / boxH;
|
||||
if (boxAspect < targetAspect) {
|
||||
boxW = targetAspect * boxH;
|
||||
} else {
|
||||
boxH = boxW / targetAspect;
|
||||
}
|
||||
|
||||
// Decide if we should cap based on camera tilt (X/Y) ---
|
||||
const forward = new THREE.Vector3();
|
||||
this._threeCamera.getWorldDirection(forward);
|
||||
// |forward.z| ≈ 1 -> no tilt (look mostly perpendicular to Z=0).
|
||||
// |forward.z| ≈ 0 -> grazing the horizon (strong tilt).
|
||||
|
||||
const freeCos = Math.cos(
|
||||
THREE.MathUtils.degToRad(this._2DPlaneClampFreeTiltDeg)
|
||||
);
|
||||
const hardCos = Math.cos(
|
||||
THREE.MathUtils.degToRad(this._2DPlaneClampHardTiltDeg)
|
||||
);
|
||||
const tiltCos = Math.abs(forward.z);
|
||||
|
||||
// Map tiltCos ∈ [hardCos, freeCos] to w ∈ [1, 0]
|
||||
let w = 0;
|
||||
if (tiltCos <= hardCos)
|
||||
w = 1; // fully clamped
|
||||
else if (tiltCos >= freeCos)
|
||||
w = 0; // no clamp
|
||||
else w = (freeCos - tiltCos) / (freeCos - hardCos);
|
||||
|
||||
// Ease it
|
||||
w = Math.pow(w, this._2DPlaneClampRampPower);
|
||||
|
||||
// Interpolate Infinity→base via 1/w (bounded):
|
||||
const BIG = 1e12; // “practically infinite”
|
||||
const denom = Math.max(w, 1e-6);
|
||||
const effectiveMaxH = Math.min(
|
||||
BIG,
|
||||
this._2DPlaneMaxDrawingDistance / denom
|
||||
);
|
||||
|
||||
// Apply the max height.
|
||||
if (effectiveMaxH < BIG) {
|
||||
const clampedH = Math.max(1e-8, Math.min(boxH, effectiveMaxH));
|
||||
if (clampedH !== boxH) {
|
||||
boxH = clampedH;
|
||||
boxW = targetAspect * boxH; // keep aspect
|
||||
}
|
||||
}
|
||||
|
||||
return [boxW, boxH];
|
||||
}
|
||||
|
||||
private _get2DPlanePosition(boxH: number): [number, number] {
|
||||
if (!this._threeCamera) return [0, 0];
|
||||
|
||||
// Choose the plane position (anchor to bottom of screen, heading-invariant) ---
|
||||
const bottomLeft = projectNDCToZ0(this._threeCamera, -1, -1);
|
||||
const bottomRight = projectNDCToZ0(this._threeCamera, +1, -1);
|
||||
|
||||
let cx: number, cy: number;
|
||||
|
||||
if (bottomLeft && bottomRight) {
|
||||
// Midpoint of the bottom-of-screen segment on Z=0:
|
||||
const mx = 0.5 * (bottomLeft.x + bottomRight.x);
|
||||
const my = 0.5 * (bottomLeft.y + bottomRight.y);
|
||||
|
||||
// Tangent along the bottom line (unit):
|
||||
let dx = bottomRight.x - bottomLeft.x;
|
||||
let dy = bottomRight.y - bottomLeft.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
dx /= len;
|
||||
dy /= len;
|
||||
|
||||
// Inward normal n = +90° rotation of d in XY plane:
|
||||
// d = (dx, dy) -> n = (-dy, dx)
|
||||
let nx = -dy;
|
||||
let ny = dx;
|
||||
|
||||
// Ensure n points "into the screen":
|
||||
const midIn = projectNDCToZ0(this._threeCamera, 0, -0.5);
|
||||
if (midIn) {
|
||||
const vx = midIn.x - mx;
|
||||
const vy = midIn.y - my;
|
||||
if (vx * nx + vy * ny < 0) {
|
||||
nx = -nx;
|
||||
ny = -ny;
|
||||
}
|
||||
}
|
||||
|
||||
// Place the plane so its bottom edge lies on the bottom-of-screen line:
|
||||
cx = mx + nx * (boxH * 0.5);
|
||||
cy = my + ny * (boxH * 0.5);
|
||||
} else {
|
||||
// Fallback to the camera center projected on Z=0 if bottom line not visible:
|
||||
const centerRay = projectNDCToZ0(this._threeCamera, 0, 0);
|
||||
if (centerRay) {
|
||||
cx = centerRay.x;
|
||||
cy = centerRay.y;
|
||||
} else {
|
||||
// Fallback to the camera position if the center ray is not visible:
|
||||
cx = this._threeCamera.position.x;
|
||||
cy = this._threeCamera.position.y;
|
||||
}
|
||||
}
|
||||
return [cx, cy];
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
// Update the 3D camera position and rotation.
|
||||
if (this._threeCamera) {
|
||||
const angle = -gdjs.toRad(this._layer.getCameraRotation());
|
||||
this._threeCamera.position.x = this._layer.getCameraX();
|
||||
this._threeCamera.position.y = -this._layer.getCameraY(); // scene is mirrored on Y
|
||||
this._threeCamera.rotation.z = angle;
|
||||
|
||||
if (this._threeCamera instanceof THREE.OrthographicCamera) {
|
||||
this._threeCamera.zoom = this._layer.getCameraZoom();
|
||||
this._threeCamera.updateProjectionMatrix();
|
||||
this._threeCamera.position.z = this._layer.getCameraZ(null);
|
||||
} else {
|
||||
this._threeCamera.position.z = this._layer.getCameraZ(
|
||||
this._threeCamera.fov
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let effectivePixiZoom = 1;
|
||||
const angle = -gdjs.toRad(this._layer.getCameraRotation());
|
||||
const angleCosValue = Math.cos(angle);
|
||||
const angleSinValue = Math.sin(angle);
|
||||
|
||||
// Update the 2D plane in the 3D world position, size and rotation,
|
||||
// and update the 2D Pixi container position, size and rotation.
|
||||
if (this._threeCamera && this._threePlaneMesh) {
|
||||
const [boxW, boxH] = this._get2DPlaneSize();
|
||||
|
||||
if (boxW === 0 || boxH === 0) {
|
||||
// No size means the 2D plane is not visible.
|
||||
this._threePlaneMesh.visible = false;
|
||||
} else {
|
||||
this._threePlaneMesh.visible = true;
|
||||
|
||||
const [cx, cy] = this._get2DPlanePosition(boxH);
|
||||
|
||||
// Update the 2D plane size, position and rotation (so 2D remains upright).
|
||||
// Plane size (geometry is 1×1).
|
||||
this._threePlaneMesh.scale.set(boxW, boxH, 1);
|
||||
this._threePlaneMesh.position.set(cx, -cy, 0);
|
||||
this._threePlaneMesh.rotation.set(0, 0, -angle);
|
||||
|
||||
// Update the 2D Pixi container size and rotation to match the "zoom" (which comes from the 2D plane size)
|
||||
// rotation and position.
|
||||
effectivePixiZoom = this._layer.getWidth() / boxW; // == height/boxH
|
||||
this._pixiContainer.scale.set(effectivePixiZoom, effectivePixiZoom);
|
||||
this._pixiContainer.rotation = angle;
|
||||
|
||||
const followX = cx;
|
||||
const followY = -cy;
|
||||
const centerX2d =
|
||||
followX * effectivePixiZoom * angleCosValue -
|
||||
followY * effectivePixiZoom * angleSinValue;
|
||||
const centerY2d =
|
||||
followX * effectivePixiZoom * angleSinValue +
|
||||
followY * effectivePixiZoom * angleCosValue;
|
||||
this._pixiContainer.position.x =
|
||||
this._layer.getWidth() / 2 - centerX2d;
|
||||
this._pixiContainer.position.y =
|
||||
this._layer.getHeight() / 2 - centerY2d;
|
||||
}
|
||||
}
|
||||
|
||||
// 2D only (no 3D rendering and so no 2D plane in the 3D world):
|
||||
// Update the 2D Pixi container position, size and rotation.
|
||||
if (!this._threeCamera || !this._threePlaneMesh) {
|
||||
effectivePixiZoom = this._layer.getCameraZoom();
|
||||
this._pixiContainer.rotation = angle;
|
||||
this._pixiContainer.scale.x = effectivePixiZoom;
|
||||
this._pixiContainer.scale.y = effectivePixiZoom;
|
||||
const centerX =
|
||||
this._layer.getCameraX() * effectivePixiZoom * angleCosValue -
|
||||
this._layer.getCameraY() * effectivePixiZoom * angleSinValue;
|
||||
const centerY =
|
||||
this._layer.getCameraX() * effectivePixiZoom * angleSinValue +
|
||||
this._layer.getCameraY() * effectivePixiZoom * angleCosValue;
|
||||
this._pixiContainer.position.x = this._layer.getWidth() / 2 - centerX;
|
||||
this._pixiContainer.position.y = this._layer.getHeight() / 2 - centerY;
|
||||
}
|
||||
|
||||
// Pixel rounding for the Pixi rendering (be it for 2D only
|
||||
// or for the 2D rendering shown in the 2D plane in the 3D world).
|
||||
if (
|
||||
this._layer.getRuntimeScene().getGame().getPixelsRounding() &&
|
||||
(cosValue === 0 || sinValue === 0) &&
|
||||
Number.isInteger(zoomFactor)
|
||||
(angleCosValue === 0 || angleSinValue === 0) &&
|
||||
Number.isInteger(effectivePixiZoom)
|
||||
) {
|
||||
// Camera rounding is important for pixel perfect games.
|
||||
// Otherwise, the camera position fractional part is added to
|
||||
@@ -467,39 +919,12 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._threeCamera) {
|
||||
// TODO (3D) - improvement: handle camera rounding like down for PixiJS?
|
||||
this._threeCamera.position.x = this._layer.getCameraX();
|
||||
this._threeCamera.position.y = -this._layer.getCameraY(); // Inverted because the scene is mirrored on Y axis.
|
||||
this._threeCamera.rotation.z = angle;
|
||||
|
||||
if (this._threeCamera instanceof THREE.OrthographicCamera) {
|
||||
this._threeCamera.zoom = this._layer.getCameraZoom();
|
||||
this._threeCamera.updateProjectionMatrix();
|
||||
this._threeCamera.position.z = this._layer.getCameraZ(null);
|
||||
} else {
|
||||
this._threeCamera.position.z = this._layer.getCameraZ(
|
||||
this._threeCamera.fov
|
||||
);
|
||||
}
|
||||
|
||||
if (this._threePlaneMesh) {
|
||||
// Adapt the plane size so that it covers the whole screen.
|
||||
this._threePlaneMesh.scale.x = this._layer.getWidth() / zoomFactor;
|
||||
this._threePlaneMesh.scale.y = this._layer.getHeight() / zoomFactor;
|
||||
|
||||
// Adapt the plane position so that it's always displayed on the whole screen.
|
||||
this._threePlaneMesh.position.x = this._threeCamera.position.x;
|
||||
this._threePlaneMesh.position.y = -this._threeCamera.position.y; // Inverted because the scene is mirrored on Y axis.
|
||||
this._threePlaneMesh.rotation.z = -angle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateResolution() {
|
||||
if (this._threeEffectComposer) {
|
||||
const game = this._layer.getRuntimeScene().getGame();
|
||||
this._threeEffectComposer.setPixelRatio(window.devicePixelRatio);
|
||||
this._threeEffectComposer.setSize(
|
||||
game.getGameResolutionWidth(),
|
||||
game.getGameResolutionHeight()
|
||||
|
@@ -309,7 +309,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedFont = this._loadedFontsData.get(resourceData);
|
||||
const loadedFont = this._loadedFontsData.getFromName(resourceData.name);
|
||||
if (loadedFont) {
|
||||
this._loadedFontsData.delete(resourceData);
|
||||
}
|
||||
|
@@ -103,6 +103,10 @@ namespace gdjs {
|
||||
if (!existingTexture) {
|
||||
return this._invalidTexture;
|
||||
}
|
||||
if (existingTexture.destroyed) {
|
||||
logger.error('Texture for ' + resourceName + ' is not valid anymore.');
|
||||
return this._invalidTexture;
|
||||
}
|
||||
if (!existingTexture.valid) {
|
||||
logger.error(
|
||||
'Texture for ' +
|
||||
|
@@ -101,6 +101,7 @@ namespace gdjs {
|
||||
this._threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
this._threeRenderer.useLegacyLights = true;
|
||||
this._threeRenderer.autoClear = false;
|
||||
this._threeRenderer.pixelRatio = window.devicePixelRatio;
|
||||
this._threeRenderer.setSize(
|
||||
this._game.getGameResolutionWidth(),
|
||||
this._game.getGameResolutionHeight()
|
||||
@@ -761,7 +762,7 @@ namespace gdjs {
|
||||
};
|
||||
// @ts-ignore
|
||||
canvas.onwheel = function (event) {
|
||||
manager.onMouseWheel(-event.deltaY);
|
||||
manager.onMouseWheel(-event.deltaY, event.deltaX, event.deltaZ);
|
||||
};
|
||||
|
||||
// Touches:
|
||||
@@ -784,6 +785,7 @@ namespace gdjs {
|
||||
touch.pageY
|
||||
);
|
||||
manager.onTouchMove(touch.identifier, pos[0], pos[1]);
|
||||
manager.onTouchMove(touch.identifier, pos[0], pos[1]);
|
||||
// This works because touch events are sent
|
||||
// when they continue outside of the canvas.
|
||||
if (manager.isSimulatingMouseWithTouch()) {
|
||||
|
@@ -115,9 +115,10 @@ namespace gdjs {
|
||||
const runtimeLayerRenderingType = runtimeLayer.getRenderingType();
|
||||
const layerHas3DObjectsToRender = runtimeLayerRenderer.has3DObjects();
|
||||
if (
|
||||
runtimeLayerRenderingType ===
|
||||
!this._runtimeScene.getGame().isInGameEdition() &&
|
||||
(runtimeLayerRenderingType ===
|
||||
gdjs.RuntimeLayerRenderingType.TWO_D ||
|
||||
!layerHas3DObjectsToRender
|
||||
!layerHas3DObjectsToRender)
|
||||
) {
|
||||
// Render a layer with 2D rendering (PixiJS) only if layer is configured as is
|
||||
// or if there is no 3D object to render.
|
||||
|
@@ -41,18 +41,71 @@ namespace gdjs {
|
||||
return supportedCompressionMethods;
|
||||
};
|
||||
|
||||
/**
|
||||
* The desired status of the game, used for previews or in-game edition.
|
||||
* Either stored in the options generated by the preview or in the URL
|
||||
* in case of a hard reload.
|
||||
*/
|
||||
export type RuntimeGameStatus = {
|
||||
isPaused: boolean;
|
||||
isInGameEdition: boolean;
|
||||
sceneName: string | null;
|
||||
injectedExternalLayoutName: string | null;
|
||||
skipCreatingInstancesFromScene: boolean;
|
||||
eventsBasedObjectType: string | null;
|
||||
eventsBasedObjectVariantName: string | null;
|
||||
editorId: string | null;
|
||||
editorCamera3D?: EditorCameraState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the desired status of the game from the URL. Only useful for previews
|
||||
* when hard reloaded.
|
||||
*/
|
||||
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
|
||||
try {
|
||||
const url = new URL(location.href);
|
||||
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
|
||||
if (!runtimeGameStatus) return null;
|
||||
|
||||
const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
|
||||
return {
|
||||
isPaused: !!parsedRuntimeGameStatus.isPaused,
|
||||
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
|
||||
sceneName: '' + parsedRuntimeGameStatus.sceneName,
|
||||
injectedExternalLayoutName:
|
||||
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
|
||||
skipCreatingInstancesFromScene:
|
||||
!!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
|
||||
eventsBasedObjectType: parsedRuntimeGameStatus.eventsBasedObjectType,
|
||||
eventsBasedObjectVariantName:
|
||||
parsedRuntimeGameStatus.eventsBasedObjectVariantName,
|
||||
editorId: parsedRuntimeGameStatus.editorId,
|
||||
editorCamera3D: parsedRuntimeGameStatus.editorCamera3D,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Options given to the game at startup. */
|
||||
export type RuntimeGameOptions = {
|
||||
/** if true, force fullscreen. */
|
||||
forceFullscreen?: boolean;
|
||||
|
||||
/** if true, game is run as a preview launched from an editor. */
|
||||
isPreview?: boolean;
|
||||
/** The name of the external layout to create in the scene at position 0;0. */
|
||||
injectExternalLayout?: string;
|
||||
|
||||
/** if set, the status of the game to be restored. */
|
||||
initialRuntimeGameStatus?: RuntimeGameStatus;
|
||||
|
||||
/** Script files, used for hot-reloading. */
|
||||
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
|
||||
/** if true, export is a partial preview without events. */
|
||||
projectDataOnlyExport?: boolean;
|
||||
/** if true, export is a partial preview without reloading libraries. */
|
||||
shouldReloadLibraries?: boolean;
|
||||
/** if true, export is a partial preview without generating events. */
|
||||
shouldGenerateScenesEventsCode?: boolean;
|
||||
|
||||
/** if true, preview is launched from GDevelop native mobile app. */
|
||||
nativeMobileApp?: boolean;
|
||||
/** The address of the debugger server, to reach out using WebSocket. */
|
||||
@@ -139,7 +192,13 @@ namespace gdjs {
|
||||
_gameResolutionHeight: integer;
|
||||
_originalWidth: float;
|
||||
_originalHeight: float;
|
||||
_resizeMode: 'adaptWidth' | 'adaptHeight' | string;
|
||||
_resizeMode:
|
||||
| ''
|
||||
| 'scaleOuter'
|
||||
| 'adaptWidth'
|
||||
| 'adaptHeight'
|
||||
| 'native'
|
||||
| string;
|
||||
_adaptGameResolutionAtRuntime: boolean;
|
||||
_scaleMode: 'linear' | 'nearest';
|
||||
_pixelsRounding: boolean;
|
||||
@@ -171,12 +230,8 @@ namespace gdjs {
|
||||
_hasJustResumed: boolean = false;
|
||||
|
||||
//Inputs :
|
||||
_inputManager: InputManager;
|
||||
private _inputManager: InputManager;
|
||||
|
||||
/**
|
||||
* Allow to specify an external layout to insert in the first scene.
|
||||
*/
|
||||
_injectExternalLayout: any;
|
||||
_options: RuntimeGameOptions;
|
||||
|
||||
/**
|
||||
@@ -194,6 +249,7 @@ namespace gdjs {
|
||||
_sessionMetricsInitialized: boolean = false;
|
||||
_disableMetrics: boolean = false;
|
||||
_isPreview: boolean;
|
||||
_isInGameEdition: boolean;
|
||||
|
||||
/**
|
||||
* The capture manager, used to manage captures (screenshots, videos, etc...).
|
||||
@@ -203,12 +259,27 @@ namespace gdjs {
|
||||
/** True if the RuntimeGame has been disposed and should not be used anymore. */
|
||||
_wasDisposed: boolean = false;
|
||||
|
||||
_inGameEditor: InGameEditor | null;
|
||||
|
||||
/**
|
||||
* @param data The object (usually stored in data.json) containing the full project data
|
||||
* @param options The game options
|
||||
*/
|
||||
constructor(data: ProjectData, options?: RuntimeGameOptions) {
|
||||
this._options = options || {};
|
||||
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
if (this._isPreview) {
|
||||
// Check if we need to restore the state from the URL, which is used
|
||||
// when a preview is hard reloaded (search for `hardReload`).
|
||||
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
|
||||
if (runtimeGameStatusFromUrl) {
|
||||
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
|
||||
}
|
||||
}
|
||||
this._isInGameEdition =
|
||||
this._options.initialRuntimeGameStatus?.isInGameEdition || false;
|
||||
|
||||
this._variables = new gdjs.VariablesContainer(data.variables);
|
||||
this._variablesByExtensionName = new Map<
|
||||
string,
|
||||
@@ -237,7 +308,12 @@ namespace gdjs {
|
||||
getGlobalResourceNames(data),
|
||||
data.layouts
|
||||
);
|
||||
|
||||
this._inGameEditor = this._isInGameEdition
|
||||
? new gdjs.InGameEditor(this, data)
|
||||
: null;
|
||||
this._debuggerClient = gdjs.DebuggerClient
|
||||
? new gdjs.DebuggerClient(this)
|
||||
: null;
|
||||
this._effectsManager = new gdjs.EffectsManager();
|
||||
this._maxFPS = this._data.properties.maxFPS;
|
||||
this._minFPS = this._data.properties.minFPS;
|
||||
@@ -265,17 +341,12 @@ namespace gdjs {
|
||||
);
|
||||
this._sceneStack = new gdjs.SceneStack(this);
|
||||
this._inputManager = new gdjs.InputManager();
|
||||
this._injectExternalLayout = this._options.injectExternalLayout || '';
|
||||
this._debuggerClient = gdjs.DebuggerClient
|
||||
? new gdjs.DebuggerClient(this)
|
||||
: null;
|
||||
this._captureManager = gdjs.CaptureManager
|
||||
? new gdjs.CaptureManager(
|
||||
this._renderer,
|
||||
this._options.captureOptions || {}
|
||||
)
|
||||
: null;
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
this._sessionId = null;
|
||||
this._playerId = null;
|
||||
|
||||
@@ -311,6 +382,9 @@ namespace gdjs {
|
||||
* @param projectData The object (usually stored in data.json) containing the full project data
|
||||
*/
|
||||
setProjectData(projectData: ProjectData): void {
|
||||
if (this._inGameEditor) {
|
||||
this._inGameEditor.onProjectDataChange(projectData);
|
||||
}
|
||||
this._data = projectData;
|
||||
this._updateSceneAndExtensionsData();
|
||||
this._resourcesLoader.setResources(
|
||||
@@ -485,6 +559,55 @@ namespace gdjs {
|
||||
return eventsBasedObjectData;
|
||||
}
|
||||
|
||||
getEventsBasedObjectVariantData(
|
||||
type: string,
|
||||
variantName: string
|
||||
): EventsBasedObjectVariantData | null {
|
||||
const eventsBasedObjectData = this.getEventsBasedObjectData(type);
|
||||
if (!eventsBasedObjectData) {
|
||||
return null;
|
||||
}
|
||||
return gdjs.RuntimeGame._getEventsBasedObjectVariantData(
|
||||
eventsBasedObjectData,
|
||||
variantName
|
||||
);
|
||||
}
|
||||
|
||||
static _getEventsBasedObjectVariantData(
|
||||
eventsBasedObjectData: EventsBasedObjectData,
|
||||
variantName: string
|
||||
): EventsBasedObjectVariantData {
|
||||
if (!eventsBasedObjectData.defaultVariant) {
|
||||
eventsBasedObjectData.defaultVariant = {
|
||||
...eventsBasedObjectData,
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
// Legacy events-based objects don't have any instance in their default
|
||||
// variant since there wasn't a graphical editor at the time.
|
||||
// In this case, the editor doesn't allow to choose a variant, but a
|
||||
// variant may have stayed after a user rolled back the extension.
|
||||
// This variant must be ignored to match what the editor shows.
|
||||
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
|
||||
eventsBasedObjectData.defaultVariant.instances.length == 0;
|
||||
if (isForcedToOverrideEventsBasedObjectChildrenConfiguration) {
|
||||
return eventsBasedObjectData.defaultVariant;
|
||||
}
|
||||
let usedVariantData: EventsBasedObjectVariantData =
|
||||
eventsBasedObjectData.defaultVariant;
|
||||
for (
|
||||
let variantIndex = 0;
|
||||
variantIndex < eventsBasedObjectData.variants.length;
|
||||
variantIndex++
|
||||
) {
|
||||
const variantData = eventsBasedObjectData.variants[variantIndex];
|
||||
if (variantData.name === variantName) {
|
||||
usedVariantData = variantData;
|
||||
}
|
||||
}
|
||||
return usedVariantData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated to a scene.
|
||||
*
|
||||
@@ -523,6 +646,22 @@ namespace gdjs {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated to a scene.
|
||||
*
|
||||
* @param name The name of the scene.
|
||||
* @return The data associated to the scene or null if not found.
|
||||
*/
|
||||
getSceneData(sceneName: string): LayoutData | null {
|
||||
for (let i = 0, len = this._data.layouts.length; i < len; ++i) {
|
||||
const sceneData = this._data.layouts[i];
|
||||
if (sceneData.name == sceneName) {
|
||||
return sceneData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated to an external layout.
|
||||
*
|
||||
@@ -594,7 +733,7 @@ namespace gdjs {
|
||||
|
||||
this._gameResolutionWidth = width;
|
||||
this._gameResolutionHeight = height;
|
||||
if (this._adaptGameResolutionAtRuntime) {
|
||||
if (this._adaptGameResolutionAtRuntime || this._isInGameEdition) {
|
||||
if (
|
||||
gdjs.RuntimeGameRenderer &&
|
||||
gdjs.RuntimeGameRenderer.getWindowInnerWidth &&
|
||||
@@ -606,7 +745,10 @@ namespace gdjs {
|
||||
gdjs.RuntimeGameRenderer.getWindowInnerHeight();
|
||||
|
||||
// Enlarge either the width or the eight to fill the inner window space.
|
||||
if (this._resizeMode === 'adaptWidth') {
|
||||
if (this._isInGameEdition) {
|
||||
this._gameResolutionWidth = windowInnerWidth;
|
||||
this._gameResolutionHeight = windowInnerHeight;
|
||||
} else if (this._resizeMode === 'adaptWidth') {
|
||||
this._gameResolutionWidth =
|
||||
(this._gameResolutionHeight * windowInnerWidth) /
|
||||
windowInnerHeight;
|
||||
@@ -735,9 +877,9 @@ namespace gdjs {
|
||||
if (this._paused === enable) return;
|
||||
|
||||
this._paused = enable;
|
||||
if (this._inGameEditor) this._inGameEditor.activate(enable);
|
||||
if (this._debuggerClient) {
|
||||
if (this._paused) this._debuggerClient.sendGamePaused();
|
||||
else this._debuggerClient.sendGameResumed();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,11 +1056,16 @@ namespace gdjs {
|
||||
await loadAssets(onProgress);
|
||||
|
||||
await loadingScreen.unload();
|
||||
this.pause(false);
|
||||
|
||||
if (!this._isInGameEdition) {
|
||||
this.pause(false);
|
||||
}
|
||||
}
|
||||
|
||||
private _getFirstSceneName(): string {
|
||||
const firstSceneName = this._data.firstLayout;
|
||||
const firstSceneName =
|
||||
this._options.initialRuntimeGameStatus?.sceneName ||
|
||||
this._data.firstLayout;
|
||||
return this.hasScene(firstSceneName)
|
||||
? firstSceneName
|
||||
: // There is always at least a scene
|
||||
@@ -938,10 +1085,41 @@ namespace gdjs {
|
||||
this._forceGameResolutionUpdate();
|
||||
|
||||
// Load the first scene
|
||||
this._sceneStack.push(
|
||||
this._getFirstSceneName(),
|
||||
this._injectExternalLayout
|
||||
);
|
||||
const sceneName = this._getFirstSceneName();
|
||||
const externalLayoutName =
|
||||
this._options.initialRuntimeGameStatus?.injectedExternalLayoutName ||
|
||||
null;
|
||||
if (this._inGameEditor) {
|
||||
const eventsBasedObjectType =
|
||||
this._options.initialRuntimeGameStatus?.eventsBasedObjectType ||
|
||||
null;
|
||||
const eventsBasedObjectVariantName =
|
||||
this._options.initialRuntimeGameStatus
|
||||
?.eventsBasedObjectVariantName || null;
|
||||
const editorId =
|
||||
this._options.initialRuntimeGameStatus?.editorId || null;
|
||||
const editorCamera3D =
|
||||
this._options.initialRuntimeGameStatus?.editorCamera3D || null;
|
||||
this._inGameEditor.switchToSceneOrVariant(
|
||||
editorId,
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
eventsBasedObjectType,
|
||||
eventsBasedObjectVariantName,
|
||||
editorCamera3D
|
||||
);
|
||||
} else {
|
||||
if (sceneName) {
|
||||
this.getSceneStack().replace({
|
||||
sceneName,
|
||||
externalLayoutName:
|
||||
externalLayoutName === null ? undefined : externalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!externalLayoutName,
|
||||
clear: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._watermark.displayAtStartup();
|
||||
|
||||
//Uncomment to profile the first x frames of the game.
|
||||
@@ -967,15 +1145,33 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// The standard game loop
|
||||
let lastFrameSceneName: string | null = null;
|
||||
let accumulatedElapsedTime = 0;
|
||||
this._hasJustResumed = false;
|
||||
this._renderer.startGameLoop((lastCallElapsedTime) => {
|
||||
try {
|
||||
if (this._paused) {
|
||||
return true;
|
||||
// Watch the scene name to automatically update debugger when a scene is changed.
|
||||
if (this._debuggerClient) {
|
||||
const currentScene = (
|
||||
this._inGameEditor || this.getSceneStack()
|
||||
).getCurrentScene();
|
||||
if (
|
||||
currentScene &&
|
||||
currentScene.getName() !== lastFrameSceneName
|
||||
) {
|
||||
lastFrameSceneName = currentScene.getName();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the frame if we rendering frames too fast
|
||||
// If the game is edited, update the target framerate according to interactions.
|
||||
// Do it now (before frame skip), so that if a user interaction happens
|
||||
// we don't wait for a frame to pass at the current, probably very slow framerate.
|
||||
if (this._paused && this._inGameEditor) {
|
||||
this._inGameEditor.updateTargetFramerate(lastCallElapsedTime);
|
||||
}
|
||||
|
||||
// Skip the frame if we rendering frames too fast.
|
||||
accumulatedElapsedTime += lastCallElapsedTime;
|
||||
if (
|
||||
this._maxFPS > 0 &&
|
||||
@@ -992,17 +1188,36 @@ namespace gdjs {
|
||||
|
||||
// Manage resize events.
|
||||
if (this._notifyScenesForGameResolutionResize) {
|
||||
this._sceneStack.onGameResolutionResized();
|
||||
if (this._inGameEditor) {
|
||||
this._inGameEditor.onGameResolutionResized();
|
||||
} else {
|
||||
this._sceneStack.onGameResolutionResized();
|
||||
}
|
||||
this._notifyScenesForGameResolutionResize = false;
|
||||
}
|
||||
|
||||
// Render and step the scene.
|
||||
if (this._sceneStack.step(elapsedTime)) {
|
||||
this.getInputManager().onFrameEnded();
|
||||
// Render and possibly step the game.
|
||||
if (this._paused) {
|
||||
if (this._inGameEditor) {
|
||||
// The game is paused for edition: the in-game editor runs and render
|
||||
// the scene.
|
||||
this._inGameEditor.updateAndRender();
|
||||
} else {
|
||||
// The game is paused (for debugging): the rendering of the scene is done,
|
||||
// but the game logic is not executed (no full "step").
|
||||
this._sceneStack.renderWithoutStep();
|
||||
}
|
||||
} else {
|
||||
// The game is not paused (and so, not edited): both the rendering
|
||||
// and game logic (a full "step") is executed.
|
||||
if (!this._sceneStack.step(elapsedTime)) {
|
||||
return false; // Return if game asked to be stopped.
|
||||
}
|
||||
this._hasJustResumed = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
this.getInputManager().onFrameEnded();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (this._debuggerClient)
|
||||
this._debuggerClient.onUncaughtException(e);
|
||||
@@ -1323,6 +1538,37 @@ namespace gdjs {
|
||||
return this._isPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game loop is paused, for debugging/edition purposes.
|
||||
* @returns true if the current game is paused
|
||||
*/
|
||||
isPaused(): boolean {
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should display in-game edition tools or not.
|
||||
* @returns true if the current game is being edited.
|
||||
*/
|
||||
isInGameEdition(): boolean {
|
||||
return this._isInGameEdition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return in-game editor.
|
||||
*/
|
||||
getInGameEditor(): InGameEditor | null {
|
||||
return this._inGameEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum FPS of the game.
|
||||
* @param maximumFps The maximum FPS.
|
||||
*/
|
||||
setMaximumFps(maximumFps: integer) {
|
||||
this._maxFPS = maximumFps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should call GDevelop development APIs or not.
|
||||
*
|
||||
@@ -1384,29 +1630,24 @@ namespace gdjs {
|
||||
syncOptions: GetNetworkSyncDataOptions
|
||||
): GameNetworkSyncData | null {
|
||||
const syncData: GameNetworkSyncData = {
|
||||
var:
|
||||
syncOptions.syncGameVariables === false
|
||||
? undefined
|
||||
: this._variables.getNetworkSyncData(syncOptions),
|
||||
var: this._variables.getNetworkSyncData(syncOptions),
|
||||
sm: syncOptions.syncSounds
|
||||
? this.getSoundManager().getNetworkSyncData()
|
||||
: undefined,
|
||||
ss: this._sceneStack.getNetworkSyncData(syncOptions) || undefined,
|
||||
};
|
||||
|
||||
if (syncOptions.syncGameVariables !== false) {
|
||||
const extensionsVariablesSyncData = {};
|
||||
this._variablesByExtensionName.forEach((variables, extensionName) => {
|
||||
const extensionVariablesSyncData =
|
||||
variables.getNetworkSyncData(syncOptions);
|
||||
// If there is no variables to sync, don't include the extension in the sync data.
|
||||
if (extensionVariablesSyncData.length) {
|
||||
extensionsVariablesSyncData[extensionName] =
|
||||
extensionVariablesSyncData;
|
||||
}
|
||||
});
|
||||
syncData.extVar = extensionsVariablesSyncData;
|
||||
}
|
||||
const extensionsVariablesSyncData = {};
|
||||
this._variablesByExtensionName.forEach((variables, extensionName) => {
|
||||
const extensionVariablesSyncData =
|
||||
variables.getNetworkSyncData(syncOptions);
|
||||
// If there is no variables to sync, don't include the extension in the sync data.
|
||||
if (extensionVariablesSyncData.length) {
|
||||
extensionsVariablesSyncData[extensionName] =
|
||||
extensionVariablesSyncData;
|
||||
}
|
||||
});
|
||||
syncData.extVar = extensionsVariablesSyncData;
|
||||
|
||||
if (
|
||||
(!syncData.var || syncData.var.length === 0) &&
|
||||
|
@@ -1437,6 +1437,22 @@ namespace gdjs {
|
||||
return this.hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the object before any custom size is applied.
|
||||
* @return The width of the object
|
||||
*/
|
||||
getOriginalWidth(): float {
|
||||
return this.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the object before any custom size is applied.
|
||||
* @return The width of the object
|
||||
*/
|
||||
getOriginalHeight(): float {
|
||||
return this.getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the object, if applicable.
|
||||
* @param width The new width in pixels.
|
||||
|
@@ -128,13 +128,14 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Load the runtime scene from the given scene.
|
||||
* @param sceneAndExtensionsData An object containing the scene data.
|
||||
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
|
||||
* @param options Options to change what is loaded.
|
||||
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
||||
*/
|
||||
loadFromScene(
|
||||
sceneAndExtensionsData: SceneAndExtensionsData | null,
|
||||
options?: {
|
||||
excludedObjectNames?: Set<string>;
|
||||
skipCreatingInstances?: boolean;
|
||||
skipStoppingSoundsOnStartup?: boolean;
|
||||
}
|
||||
) {
|
||||
@@ -195,18 +196,17 @@ namespace gdjs {
|
||||
this.registerObject(sceneData.objects[i]);
|
||||
}
|
||||
|
||||
// Create initial instances of objects.
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true,
|
||||
{
|
||||
excludedObjectNames: options?.excludedObjectNames,
|
||||
}
|
||||
);
|
||||
//Create initial instances of objects
|
||||
if (!options || !options.skipCreatingInstances) {
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Set up the default z order (for objects created from events)
|
||||
this._setLayerDefaultZOrders();
|
||||
@@ -379,7 +379,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Step and render the scene.
|
||||
* Step (execute the game logic) and render the scene.
|
||||
* @param elapsedTime In milliseconds
|
||||
* @return true if the game loop should continue, false if a scene change/push/pop
|
||||
* or a game stop was requested.
|
||||
@@ -439,6 +439,21 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.end('callbacks and extensions (post-events)');
|
||||
}
|
||||
|
||||
this.render();
|
||||
this._isJustResumed = false;
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
/**
|
||||
* Render the scene (but do not execute the game logic).
|
||||
*/
|
||||
render() {
|
||||
if (this._profiler) {
|
||||
this._profiler.begin('objects (pre-render, effects update)');
|
||||
}
|
||||
@@ -468,21 +483,6 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
this._isJustResumed = false;
|
||||
this.render();
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PIXI container associated to the runtimeScene.
|
||||
*/
|
||||
render() {
|
||||
this._renderer.render();
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,11 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Scene stack');
|
||||
const debugLogger = new gdjs.Logger('Scene stack - Debug');
|
||||
// Comment this to see message logs and ease debugging:
|
||||
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(
|
||||
'Scene stack - Debug'
|
||||
);
|
||||
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
|
||||
|
||||
interface PushSceneOptions {
|
||||
sceneName: string;
|
||||
externalLayoutName?: string;
|
||||
getExcludedObjectNames?: (runtimeScene: RuntimeScene) => Set<string>;
|
||||
skipCreatingInstancesFromScene?: boolean;
|
||||
skipStoppingSoundsOnStartup?: boolean;
|
||||
}
|
||||
|
||||
@@ -97,10 +93,10 @@ namespace gdjs {
|
||||
renderWithoutStep(): boolean {
|
||||
this._throwIfDisposed();
|
||||
|
||||
if (this._stack.length === 0) {
|
||||
const currentScene = this.getCurrentScene();
|
||||
if (!currentScene) {
|
||||
return false;
|
||||
}
|
||||
const currentScene = this._stack[this._stack.length - 1];
|
||||
currentScene.render();
|
||||
return true;
|
||||
}
|
||||
@@ -150,10 +146,10 @@ namespace gdjs {
|
||||
|
||||
const sceneName =
|
||||
typeof options === 'string' ? options : options.sceneName;
|
||||
const getExcludedObjectNames =
|
||||
const skipCreatingInstancesFromScene =
|
||||
typeof options === 'string'
|
||||
? undefined
|
||||
: options.getExcludedObjectNames;
|
||||
? false
|
||||
: options.skipCreatingInstancesFromScene;
|
||||
const skipStoppingSoundsOnStartup =
|
||||
typeof options === 'string'
|
||||
? false
|
||||
@@ -174,7 +170,7 @@ namespace gdjs {
|
||||
return this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
getExcludedObjectNames,
|
||||
skipCreatingInstancesFromScene,
|
||||
skipStoppingSoundsOnStartup,
|
||||
});
|
||||
}
|
||||
@@ -184,7 +180,7 @@ namespace gdjs {
|
||||
this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
getExcludedObjectNames,
|
||||
skipCreatingInstancesFromScene,
|
||||
skipStoppingSoundsOnStartup,
|
||||
});
|
||||
this._isNextLayoutLoading = false;
|
||||
@@ -201,9 +197,7 @@ namespace gdjs {
|
||||
newScene.loadFromScene(
|
||||
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
|
||||
{
|
||||
excludedObjectNames: options.getExcludedObjectNames
|
||||
? options.getExcludedObjectNames(newScene)
|
||||
: undefined,
|
||||
skipCreatingInstances: options.skipCreatingInstancesFromScene,
|
||||
skipStoppingSoundsOnStartup: options.skipStoppingSoundsOnStartup,
|
||||
}
|
||||
);
|
||||
@@ -339,7 +333,8 @@ namespace gdjs {
|
||||
|
||||
this._sceneStackSyncDataToApply = null;
|
||||
|
||||
const getExcludedObjectNames = options && options.getExcludedObjectNames;
|
||||
const skipCreatingInstancesFromScene =
|
||||
!!options && !!options.preventInitialInstancesCreation;
|
||||
const skipStoppingSoundsOnStartup =
|
||||
!!options && !!options.preventSoundsStoppingOnStartup;
|
||||
|
||||
@@ -354,7 +349,7 @@ namespace gdjs {
|
||||
const sceneSyncData = sceneStackSyncData[i];
|
||||
const newScene = this.push({
|
||||
sceneName: sceneSyncData.name,
|
||||
getExcludedObjectNames,
|
||||
skipCreatingInstancesFromScene,
|
||||
skipStoppingSoundsOnStartup,
|
||||
});
|
||||
if (newScene) {
|
||||
@@ -364,7 +359,6 @@ namespace gdjs {
|
||||
hasMadeChangeToStack = true;
|
||||
return hasMadeChangeToStack;
|
||||
}
|
||||
|
||||
// If this method is called, we are a client.
|
||||
// We trust the host to be the source of truth for the scene stack.
|
||||
// So we loop through the scenes in the stack given by the host and either:
|
||||
@@ -380,10 +374,9 @@ namespace gdjs {
|
||||
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
|
||||
);
|
||||
// We have fewer scenes in the stack than the host, let's add the scene.
|
||||
// By definition, there is nothing to clear because we're already at the top of the stack.
|
||||
const newScene = this.push({
|
||||
sceneName: sceneSyncData.name,
|
||||
getExcludedObjectNames,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
@@ -397,23 +390,16 @@ namespace gdjs {
|
||||
debugLogger.info(
|
||||
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} is not the same as the expected ${
|
||||
sceneSyncData.name
|
||||
}, replacing it.`
|
||||
}, replacing.`
|
||||
);
|
||||
// The scene does not correspond to the scene at this position in our stack
|
||||
// Let's unload everything after this position to reconstruct the stack.
|
||||
if (this._stack.length > i + 1) {
|
||||
debugLogger.info(
|
||||
`Unloading ${this._stack.length - (i + 1)} scenes after position ${i}.`
|
||||
);
|
||||
this.pop(this._stack.length - (i + 1));
|
||||
}
|
||||
// Let's unload everything after this position to recreate the stack.
|
||||
|
||||
const newScene = this.replace({
|
||||
sceneName: sceneSyncData.name,
|
||||
clear: false,
|
||||
getExcludedObjectNames,
|
||||
clear: true,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
}
|
||||
@@ -447,26 +433,16 @@ namespace gdjs {
|
||||
debugLogger.info(
|
||||
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} has a different networkId ${
|
||||
sceneAtThisPositionInOurStack.networkId
|
||||
} than the expected ${sceneSyncData.networkId}, replacing it.`
|
||||
} than the expected ${sceneSyncData.networkId}, replacing.`
|
||||
);
|
||||
// The scene is in the stack but has a different networkId
|
||||
// This can happen if the host has restarted the scene
|
||||
// We can't just update the networkId of the scene in the stack
|
||||
// We need to replace it with a new scene.
|
||||
|
||||
// Like for the case where it's a totally different scene,
|
||||
// we need to unload everything after this position to reconstruct the stack.
|
||||
if (this._stack.length > i + 1) {
|
||||
debugLogger.info(
|
||||
`Unloading ${this._stack.length - (i + 1)} scenes after position ${i}.`
|
||||
);
|
||||
this.pop(this._stack.length - (i + 1));
|
||||
}
|
||||
|
||||
// We need to replace it with a new scene
|
||||
const newScene = this.replace({
|
||||
sceneName: sceneSyncData.name,
|
||||
clear: false,
|
||||
getExcludedObjectNames,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
if (newScene) {
|
||||
newScene.networkId = sceneSyncData.networkId;
|
||||
|
@@ -190,15 +190,13 @@ namespace gdjs {
|
||||
this.setWidth(initialInstanceData.width);
|
||||
this.setHeight(initialInstanceData.height);
|
||||
}
|
||||
if (initialInstanceData.opacity !== undefined) {
|
||||
this.setOpacity(initialInstanceData.opacity);
|
||||
}
|
||||
if (initialInstanceData.flippedX) {
|
||||
this.flipX(initialInstanceData.flippedX);
|
||||
}
|
||||
if (initialInstanceData.flippedY) {
|
||||
this.flipY(initialInstanceData.flippedY);
|
||||
}
|
||||
this.setOpacity(
|
||||
initialInstanceData.opacity === undefined
|
||||
? 255
|
||||
: initialInstanceData.opacity
|
||||
);
|
||||
this.flipX(!!initialInstanceData.flippedX);
|
||||
this.flipY(!!initialInstanceData.flippedY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -853,6 +851,14 @@ namespace gdjs {
|
||||
this.setHeight(newHeight);
|
||||
}
|
||||
|
||||
override getOriginalWidth(): float {
|
||||
return this._renderer.getUnscaledWidth() * this._preScale;
|
||||
}
|
||||
|
||||
override getOriginalHeight(): float {
|
||||
return this._renderer.getUnscaledHeight() * this._preScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale on X and Y axis of the object.
|
||||
*
|
||||
|
6
GDJS/Runtime/types/global-three-addons.d.ts
vendored
6
GDJS/Runtime/types/global-three-addons.d.ts
vendored
@@ -2,7 +2,10 @@ import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
|
||||
import { SelectionBox } from 'three/examples/jsm/interactive/SelectionBox';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
|
||||
import { Pass } from 'three/examples/jsm/postprocessing/Pass';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
||||
@@ -21,7 +24,10 @@ declare global {
|
||||
GLTF,
|
||||
DRACOLoader,
|
||||
SkeletonUtils,
|
||||
TransformControls,
|
||||
SelectionBox,
|
||||
EffectComposer,
|
||||
OutlinePass,
|
||||
Pass,
|
||||
RenderPass,
|
||||
ShaderPass,
|
||||
|
29
GDJS/Runtime/types/global-types.d.ts
vendored
29
GDJS/Runtime/types/global-types.d.ts
vendored
@@ -76,32 +76,3 @@ declare namespace gdjs {
|
||||
|
||||
/** The global cc object from Cocos2D-Js. */
|
||||
declare var cc: any;
|
||||
|
||||
/**
|
||||
* Fixes https://github.com/microsoft/TypeScript/issues/16655 for `Array.prototype.filter()`
|
||||
* For example, using the fix the type of `bar` is `string[]` in the below snippet as it should be.
|
||||
*
|
||||
* const foo: (string | null | undefined)[] = [];
|
||||
* const bar = foo.filter(Boolean);
|
||||
*
|
||||
* For related definitions, see https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts
|
||||
*
|
||||
* Original licenses apply, see
|
||||
* - https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt
|
||||
* - https://stackoverflow.com/help/licensing
|
||||
*/
|
||||
|
||||
/** See https://stackoverflow.com/a/51390763/1470607 */
|
||||
type Falsy = false | 0 | '' | null | undefined;
|
||||
|
||||
interface Array<T> {
|
||||
/**
|
||||
* Returns the elements of an array that meet the condition specified in a callback function.
|
||||
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
|
||||
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
|
||||
*/
|
||||
filter<S extends T>(
|
||||
predicate: BooleanConstructor,
|
||||
thisArg?: any
|
||||
): Exclude<S, Falsy>[];
|
||||
}
|
||||
|
36
GDJS/Runtime/types/project-data.d.ts
vendored
36
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -18,6 +18,7 @@ declare interface ProjectData {
|
||||
layouts: LayoutData[];
|
||||
externalLayouts: ExternalLayoutData[];
|
||||
eventsFunctionsExtensions: EventsFunctionsExtensionData[];
|
||||
areEffectsHiddenInEditor?: boolean;
|
||||
}
|
||||
|
||||
declare interface EventsFunctionsVariablesData {
|
||||
@@ -43,9 +44,8 @@ declare type GetNetworkSyncDataOptions = {
|
||||
playerNumber?: number;
|
||||
isHost?: boolean;
|
||||
syncObjectIdentifiers?: boolean;
|
||||
shouldExcludeVariableFromData?: (variable: Variable) => boolean;
|
||||
syncAllVariables?: boolean;
|
||||
syncAllBehaviors?: boolean;
|
||||
syncGameVariables?: boolean;
|
||||
syncSceneTimers?: boolean;
|
||||
syncOnceTriggers?: boolean;
|
||||
syncSounds?: boolean;
|
||||
@@ -58,12 +58,11 @@ declare type GetNetworkSyncDataOptions = {
|
||||
|
||||
declare type UpdateFromNetworkSyncDataOptions = {
|
||||
clearSceneStack?: boolean;
|
||||
getExcludedObjectNames?: (runtimeScene: RuntimeScene) => Set<string>;
|
||||
preventInitialInstancesCreation?: boolean;
|
||||
preventSoundsStoppingOnStartup?: boolean;
|
||||
clearInputs?: boolean;
|
||||
keepControl?: boolean;
|
||||
ignoreVariableOwnership?: boolean;
|
||||
shouldExcludeVariableFromUpdate?: (variable: Variable) => boolean;
|
||||
};
|
||||
|
||||
/** Object containing basic properties for all objects synchronizing over the network. */
|
||||
@@ -290,6 +289,21 @@ declare interface LayoutData extends InstanceContainerData {
|
||||
usedResources: ResourceReference[];
|
||||
resourcesPreloading?: 'at-startup' | 'never' | 'inherit';
|
||||
resourcesUnloading?: 'at-scene-exit' | 'never' | 'inherit';
|
||||
uiSettings: InstancesEditorSettings;
|
||||
}
|
||||
|
||||
declare interface InstancesEditorSettings {
|
||||
grid: boolean;
|
||||
gridType: 'rectangular' | 'isometric';
|
||||
gridWidth: float;
|
||||
gridHeight: float;
|
||||
gridDepth?: float;
|
||||
gridOffsetX: float;
|
||||
gridOffsetY: float;
|
||||
gridOffsetZ?: float;
|
||||
gridColor: int;
|
||||
gridAlpha: float;
|
||||
snap: boolean;
|
||||
}
|
||||
|
||||
declare interface LayoutNetworkSyncData {
|
||||
@@ -385,6 +399,8 @@ declare interface EventsBasedObjectVariantData extends InstanceContainerData {
|
||||
instances: InstanceData[];
|
||||
objects: ObjectData[];
|
||||
layers: LayerData[];
|
||||
usedResources: ResourceReference[];
|
||||
editionSettings: InstancesEditorSettings;
|
||||
}
|
||||
|
||||
declare interface BehaviorSharedData {
|
||||
@@ -396,13 +412,17 @@ declare interface ExternalLayoutData {
|
||||
name: string;
|
||||
associatedLayout: string;
|
||||
instances: InstanceData[];
|
||||
editionSettings: InstancesEditorSettings;
|
||||
}
|
||||
|
||||
declare interface InstanceData {
|
||||
declare interface InstancePersistentUuidData {
|
||||
persistentUuid: string;
|
||||
}
|
||||
|
||||
declare interface InstanceData extends InstancePersistentUuidData {
|
||||
layer: string;
|
||||
locked: boolean;
|
||||
locked?: boolean;
|
||||
sealed?: boolean;
|
||||
name: string;
|
||||
|
||||
x: number;
|
||||
@@ -453,8 +473,10 @@ declare interface LayerData {
|
||||
camera3DFieldOfView?: float;
|
||||
camera3DFarPlaneDistance?: float;
|
||||
camera3DNearPlaneDistance?: float;
|
||||
camera2DPlaneMaxDrawingDistance?: float;
|
||||
isLightingLayer: boolean;
|
||||
followBaseLayerCamera: boolean;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
declare interface CameraData {
|
||||
@@ -499,7 +521,7 @@ declare interface ProjectPropertiesData {
|
||||
pixelsRounding: boolean;
|
||||
antialiasingMode: 'none' | 'MSAA';
|
||||
antialisingEnabledOnMobile: boolean;
|
||||
sizeOnStartupMode: string;
|
||||
sizeOnStartupMode: '' | 'scaleOuter' | 'adaptWidth' | 'adaptHeight';
|
||||
version: string;
|
||||
name: string;
|
||||
author: string;
|
||||
|
2
GDJS/Runtime/types/save-state.d.ts
vendored
2
GDJS/Runtime/types/save-state.d.ts
vendored
@@ -4,6 +4,6 @@ declare type SceneSaveState = {
|
||||
};
|
||||
|
||||
declare type GameSaveState = {
|
||||
gameNetworkSyncData: GameNetworkSyncData | null;
|
||||
gameNetworkSyncData: GameNetworkSyncData;
|
||||
layoutNetworkSyncDatas: SceneSaveState[];
|
||||
};
|
||||
|
@@ -126,38 +126,26 @@ namespace gdjs {
|
||||
getNetworkSyncData(
|
||||
syncOptions: GetNetworkSyncDataOptions
|
||||
): UnnamedVariableNetworkSyncData | undefined {
|
||||
const syncedPlayerNumber = syncOptions.playerNumber;
|
||||
const isHost = syncOptions.isHost;
|
||||
const variableOwner = this.getPlayerOwnership();
|
||||
if (
|
||||
// Variable undefined.
|
||||
this.isUndefinedInContainer()
|
||||
)
|
||||
return;
|
||||
|
||||
const variableOwner = this.getPlayerOwnership();
|
||||
|
||||
if (syncOptions.shouldExcludeVariableFromData) {
|
||||
// Saving for "save state": serialize all variables unless excluded.
|
||||
if (syncOptions.shouldExcludeVariableFromData(this)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Saving for "multiplayer": only serialize the variable if owned by the player.
|
||||
const syncedPlayerNumber = syncOptions.playerNumber;
|
||||
const isHost = syncOptions.isHost;
|
||||
|
||||
if (
|
||||
this.isUndefinedInContainer() ||
|
||||
// If we force sync everything, we don't look at the ownership.
|
||||
(!syncOptions.syncAllVariables &&
|
||||
// Variable marked as not to be synchronized.
|
||||
variableOwner === null ||
|
||||
// Getting sync data for a specific player:
|
||||
(syncedPlayerNumber !== undefined &&
|
||||
// Variable is owned by host but this player number is not the host.
|
||||
variableOwner === 0 &&
|
||||
!isHost) ||
|
||||
// Variable is owned by a player but not getting sync data for this player number.
|
||||
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
|
||||
) {
|
||||
// In those cases, the variable should not be synchronized.
|
||||
return;
|
||||
}
|
||||
(variableOwner === null ||
|
||||
// Getting sync data for a specific player:
|
||||
(syncedPlayerNumber !== undefined &&
|
||||
// Variable is owned by host but this player number is not the host.
|
||||
variableOwner === 0 &&
|
||||
!isHost) ||
|
||||
// Variable is owned by a player but not getting sync data for this player number.
|
||||
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)))
|
||||
) {
|
||||
// In those cases, the variable should not be synchronized.
|
||||
return;
|
||||
}
|
||||
|
||||
const variableType = this.getType();
|
||||
@@ -264,13 +252,6 @@ namespace gdjs {
|
||||
const variableData =
|
||||
gdjs.Variable.getVariableDataFromNetworkSyncData(networkSyncData);
|
||||
|
||||
if (
|
||||
options.shouldExcludeVariableFromUpdate &&
|
||||
options.shouldExcludeVariableFromUpdate(this)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.ignoreVariableOwnership) {
|
||||
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
|
||||
|
||||
|
@@ -19,6 +19,7 @@ const allowedExtensions = [
|
||||
'.map',
|
||||
'.wasm',
|
||||
'.txt',
|
||||
'.png',
|
||||
];
|
||||
|
||||
// These extensions will be built with esbuild (the other will be copied).
|
||||
@@ -42,6 +43,7 @@ const untransformedPaths = [
|
||||
'GDJS/Runtime/FacebookInstantGames',
|
||||
'GDJS/Runtime/libs/CocoonJS',
|
||||
'GDJS/Runtime/libs/rbush.js',
|
||||
'GDJS/Runtime/InGameEditor/Resources/primitivedrawingicon.png',
|
||||
|
||||
// Extensions pre-built files:
|
||||
'Extensions/Leaderboards/sha256.js',
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"firstLayout": "",
|
||||
"gdVersion": {
|
||||
"build": 242,
|
||||
"build": 241,
|
||||
"major": 5,
|
||||
"minor": 5,
|
||||
"revision": 0
|
||||
@@ -362,8 +362,7 @@
|
||||
"gridAlpha": 0.8,
|
||||
"snap": false,
|
||||
"zoomFactor": 0.4633111824514028,
|
||||
"windowMask": false,
|
||||
"selectedLayer": ""
|
||||
"windowMask": false
|
||||
},
|
||||
"objectsGroups": [],
|
||||
"variables": [
|
||||
@@ -404,11 +403,6 @@
|
||||
"name": "Variable",
|
||||
"type": "number",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"name": "VariableExcludedFromSave",
|
||||
"type": "number",
|
||||
"value": 10
|
||||
}
|
||||
],
|
||||
"instances": [
|
||||
@@ -454,8 +448,8 @@
|
||||
"name": "Joystick",
|
||||
"persistentUuid": "855d809a-f954-4072-86d5-1d6f0b24a066",
|
||||
"width": 0,
|
||||
"x": 131,
|
||||
"y": 604,
|
||||
"x": 176,
|
||||
"y": 576,
|
||||
"zOrder": 2,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
@@ -844,118 +838,6 @@
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "Background",
|
||||
"name": "NewText7",
|
||||
"persistentUuid": "02ff696e-6d03-466c-9e4a-f993c4c45958",
|
||||
"width": 0,
|
||||
"x": 159,
|
||||
"y": 35,
|
||||
"zOrder": 39,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "749601b7-c699-47a6-81b8-d19b7cd5bca9",
|
||||
"width": 0,
|
||||
"x": 704,
|
||||
"y": 403,
|
||||
"zOrder": 40,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "502f29fe-fca1-4625-b194-49a701bda9cd",
|
||||
"width": 0,
|
||||
"x": 723,
|
||||
"y": 323,
|
||||
"zOrder": 41,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "65d4f46c-339a-4a2b-9870-799566c2456b",
|
||||
"width": 0,
|
||||
"x": 846,
|
||||
"y": 205,
|
||||
"zOrder": 42,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "3e848abb-3ab3-48ce-9176-23cd9be84fde",
|
||||
"width": 0,
|
||||
"x": 1094,
|
||||
"y": 185,
|
||||
"zOrder": 43,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "47351cf9-0485-4388-958c-94024dd13e9f",
|
||||
"width": 0,
|
||||
"x": 1219,
|
||||
"y": 263,
|
||||
"zOrder": 44,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"customSize": false,
|
||||
"height": 0,
|
||||
"keepRatio": true,
|
||||
"layer": "",
|
||||
"name": "Coins",
|
||||
"persistentUuid": "8b305063-3f81-4ef4-accc-1f36ad1bed2d",
|
||||
"width": 0,
|
||||
"x": 1228,
|
||||
"y": 362,
|
||||
"zOrder": 45,
|
||||
"numberProperties": [],
|
||||
"stringProperties": [],
|
||||
"initialVariables": []
|
||||
}
|
||||
],
|
||||
"objects": [
|
||||
@@ -1119,14 +1001,7 @@
|
||||
"updateIfNotVisible": false,
|
||||
"variables": [],
|
||||
"effects": [],
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "SaveConfiguration",
|
||||
"type": "SaveState::SaveConfiguration",
|
||||
"defaultProfilePersistence": "Persisted",
|
||||
"persistedInProfiles": "coins"
|
||||
}
|
||||
],
|
||||
"behaviors": [],
|
||||
"animations": [
|
||||
{
|
||||
"name": "",
|
||||
@@ -1284,14 +1159,7 @@
|
||||
"variant": "",
|
||||
"variables": [],
|
||||
"effects": [],
|
||||
"behaviors": [
|
||||
{
|
||||
"name": "SaveConfiguration",
|
||||
"type": "SaveState::SaveConfiguration",
|
||||
"defaultProfilePersistence": "DoNotSave",
|
||||
"persistedInProfiles": ""
|
||||
}
|
||||
],
|
||||
"behaviors": [],
|
||||
"content": {},
|
||||
"childrenContent": {
|
||||
"Border": {
|
||||
@@ -2145,49 +2013,6 @@
|
||||
"lineHeight": 0,
|
||||
"color": "0;0;0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetStoreId": "",
|
||||
"bold": false,
|
||||
"italic": false,
|
||||
"name": "NewText7",
|
||||
"smoothed": true,
|
||||
"type": "TextObject::Text",
|
||||
"underlined": false,
|
||||
"variables": [],
|
||||
"effects": [],
|
||||
"behaviors": [],
|
||||
"string": "Excluded Variable",
|
||||
"font": "",
|
||||
"textAlignment": "left",
|
||||
"characterSize": 20,
|
||||
"color": {
|
||||
"b": 0,
|
||||
"g": 0,
|
||||
"r": 0
|
||||
},
|
||||
"content": {
|
||||
"bold": false,
|
||||
"isOutlineEnabled": false,
|
||||
"isShadowEnabled": false,
|
||||
"italic": false,
|
||||
"outlineColor": "255;255;255",
|
||||
"outlineThickness": 2,
|
||||
"shadowAngle": 90,
|
||||
"shadowBlurRadius": 2,
|
||||
"shadowColor": "0;0;0",
|
||||
"shadowDistance": 4,
|
||||
"shadowOpacity": 127,
|
||||
"smoothed": true,
|
||||
"underlined": false,
|
||||
"text": "Excluded Variable",
|
||||
"font": "",
|
||||
"textAlignment": "left",
|
||||
"verticalTextAlignment": "top",
|
||||
"characterSize": 20,
|
||||
"lineHeight": 0,
|
||||
"color": "0;0;0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"objectsFolderStructure": {
|
||||
@@ -2220,9 +2045,6 @@
|
||||
{
|
||||
"objectName": "NewText6"
|
||||
},
|
||||
{
|
||||
"objectName": "NewText7"
|
||||
},
|
||||
{
|
||||
"objectName": "FlagBlue"
|
||||
},
|
||||
@@ -2295,23 +2117,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::SetVariableSaveConfiguration"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"VariableExcludedFromSave",
|
||||
"no",
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
@@ -2348,12 +2153,11 @@
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::CreateGameSaveStateInStorage"
|
||||
"value": "SaveState::SaveGameSnapshotToStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"test\"",
|
||||
""
|
||||
"\"test\""
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -2383,143 +2187,10 @@
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::RestoreGameSaveStateFromStorage"
|
||||
"value": "SaveState::LoadGameSnapshotFromStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"test\"",
|
||||
"",
|
||||
"no"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Comment",
|
||||
"color": {
|
||||
"b": 109,
|
||||
"g": 230,
|
||||
"r": 255,
|
||||
"textB": 0,
|
||||
"textG": 0,
|
||||
"textR": 0
|
||||
},
|
||||
"comment": "Press s to save and l to load manually. Check how the \"VariableExcludedFromSave\" won't be impacted."
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "KeyFromTextJustPressed"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"s\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::CreateGameSaveStateInStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"manual-save\"",
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "KeyFromTextJustPressed"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"l\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::RestoreGameSaveStateFromStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"manual-save\"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Comment",
|
||||
"color": {
|
||||
"b": 109,
|
||||
"g": 230,
|
||||
"r": 255,
|
||||
"textB": 0,
|
||||
"textG": 0,
|
||||
"textR": 0
|
||||
},
|
||||
"comment": "Press c to just save the coins and press x to load only the coins. Check how the rest won't be impacted."
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "KeyFromTextJustPressed"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"c\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::CreateGameSaveStateInStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"coins-save\"",
|
||||
"\"coins\""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "KeyFromTextJustPressed"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"x\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "SaveState::RestoreGameSaveStateFromStorage"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"coins-save\"",
|
||||
"\"coins\"",
|
||||
""
|
||||
"\"test\""
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -2561,7 +2232,6 @@
|
||||
},
|
||||
"parameters": [
|
||||
">=",
|
||||
">",
|
||||
"0"
|
||||
]
|
||||
}
|
||||
@@ -2588,7 +2258,6 @@
|
||||
"value": "SaveState::TimeSinceLastSave"
|
||||
},
|
||||
"parameters": [
|
||||
"<",
|
||||
"<",
|
||||
"0"
|
||||
]
|
||||
@@ -2617,7 +2286,6 @@
|
||||
},
|
||||
"parameters": [
|
||||
">=",
|
||||
">",
|
||||
"0"
|
||||
]
|
||||
}
|
||||
@@ -2644,7 +2312,6 @@
|
||||
"value": "SaveState::TimeSinceLastLoad"
|
||||
},
|
||||
"parameters": [
|
||||
"<",
|
||||
"<",
|
||||
"0"
|
||||
]
|
||||
@@ -3041,84 +2708,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "TextContainerCapability::TextContainerBehavior::SetValue"
|
||||
},
|
||||
"parameters": [
|
||||
"NewText7",
|
||||
"Text",
|
||||
"=",
|
||||
"\"Excluded variable: \" + VariableExcludedFromSave"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "DepartScene"
|
||||
},
|
||||
"parameters": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "ResetTimer"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"ExcludedVariableIncreaseTimer\""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BuiltinCommonInstructions::Standard",
|
||||
"conditions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "CompareTimer"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"ExcludedVariableIncreaseTimer\"",
|
||||
">=",
|
||||
"0.5"
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": {
|
||||
"value": "ResetTimer"
|
||||
},
|
||||
"parameters": [
|
||||
"",
|
||||
"\"ExcludedVariableIncreaseTimer\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": "SetNumberVariable"
|
||||
},
|
||||
"parameters": [
|
||||
"VariableExcludedFromSave",
|
||||
"+",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
@@ -3230,10 +2819,6 @@
|
||||
"name": "Resizable",
|
||||
"type": "ResizableCapability::ResizableBehavior"
|
||||
},
|
||||
{
|
||||
"name": "SaveConfiguration",
|
||||
"type": "SaveState::SaveConfiguration"
|
||||
},
|
||||
{
|
||||
"name": "Scale",
|
||||
"type": "ScalableCapability::ScalableBehavior"
|
||||
@@ -13765,7 +13350,6 @@
|
||||
}
|
||||
],
|
||||
"instances": [],
|
||||
"editionSettings": {},
|
||||
"eventsFunctions": [
|
||||
{
|
||||
"fullName": "",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user