Compare commits

...

41 Commits

Author SHA1 Message Date
Cursor Agent
c0ab3ccc50 Refactor MainFrame to extract EditorsPane component
Co-authored-by: florian <florian@gdevelop.io>
2025-07-13 19:00:06 +00:00
Florian Rival
aded08471d Adapt sentences displaying free AI requests
Don't show in changelog
2025-07-13 19:40:10 +02:00
Florian Rival
cccb59b1c5 Fix AI agent not working with games with a lot of extensions or that were too big 2025-07-12 16:50:38 +02:00
D8H
3592fb7e62 Fix hemisphere light orientation when the top is set on Z+ (#7708) 2025-07-10 12:09:18 +02:00
D8H
307c92991c Fix shadow casting and receiving that were inverted for 3D boxes (#7704) 2025-07-09 13:43:01 +02:00
Florian Rival
4b3f077669 Bump newIDE version 2025-07-08 10:59:24 +02:00
github-actions[bot]
352bae518e Update translations [skip ci] (#7689)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-07-08 10:58:42 +02:00
D8H
c958f4d522 Fix directional light orientation and missing settings in the 3D cube editor (#7701) 2025-07-07 15:10:11 +02:00
D8H
35bbb37ad2 Fix hot-reloading of custom objects (#7698) 2025-07-07 08:37:08 +02:00
D8H
1d48acc841 Fix custom objects being destroyed too soon (#7695) 2025-07-07 08:27:00 +02:00
Florian Rival
87702edccc Show an highlight on newly generated AI events to make it easier to find them (#7696) 2025-07-06 19:20:41 +02:00
Florian Rival
1f0ba7c19a Update package.json 2025-07-04 16:45:43 +02:00
Florian Rival
b4d08a99ad Add setting to customize shadow bias 2025-07-04 16:44:55 +02:00
Florian Rival
8acaa06e42 Fix formatting [ci skip] [skip ci]
Don't show in changelog
2025-07-04 14:11:13 +02:00
Florian Rival
27ee85b5d4 Add shadow biais to avoid shadow acne
Don't show in changelog
2025-07-04 12:48:38 +02:00
Arthur Pacaud (arthuro555)
bbe2d1854e Update "Pick all instances" to avoid crashes when a lot of objects are picked (#7439) 2025-07-03 23:29:44 +02:00
Florian Rival
d338690ff5 Fix loading of 3D resources
This was a regresssion since the refactoring to allow unloading resources

Don't show in changelog
2025-07-03 17:56:30 +02:00
Florian Rival
571a6d8c1a Fix directional light settings not applied when scene starts
Don't show in changelog
2025-07-03 15:33:06 +02:00
Florian Rival
ddb5157c0a Bump newIDE version 2025-07-03 13:53:29 +02:00
Florian Rival
64f01354bc Fix formatting 2025-07-03 13:50:16 +02:00
Florian Rival
37fd99e542 Add user friendly labels to select fields in object and effect properties 2025-07-03 13:09:43 +02:00
Florian Rival
23be4a5849 Update shadow quality choices to be lowercase
Don't show in changelog
2025-07-03 12:34:57 +02:00
github-actions[bot]
64c0ee8f98 Update translations [skip ci] (#7683)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-07-03 12:23:19 +02:00
D8H
e5ecce3abf [Platformer] Fix wrongly allowed double jump when jumping from a slope (#7687) 2025-07-02 16:48:38 +02:00
ViktorVovk
5c71a4da56 Allow to unload scene resources when a scene is exited (#7381)
* This adds two new settings, available in the Project Properties and in the Scene Properties dialog, to allow to specify the strategy for preloading resources of the scenes and unloading them. By default, a game will preload in background the resources of all scenes. It will never unload these resources (so scene switching is fast).
* You can now choose to unload the resources of a scene when the scene is left using the "Resources unloading" field. If the scene is launched again later, it will load its resources again.
* You can also choose to change the preloading to disable it for all scenes (by modifying the setting in the project properties) or enable it/disable it on a scene by scene basis. This can be useful for large or modular games where you anticipate the player to only play some scenes, or if you want to reduce the resources that needs to be loaded on a web game.
2025-07-02 16:09:52 +02:00
Neyl
dff99b79cb Add basic support for shadows (#7592)
* Shadows are rendered for 3D objects when a **Directional Light** is set up on your scene layer - which is now the case by default for new games and new layers: they will have both a Directional Light and an Ambient Light. This renders shadows like it could be done by the sun.
* 3D models and 3D cubes are now casting shadows. To see them, you must ensure you the "Standard" material type in their configuration (and not the "Basic", which means they don't react to light) and be sure to enable "Shadow casting" and "Shadow receiving". This is done by default for new objects you create or import from the Asset Store.
* Shadows are rendered around the camera on an area that is large enough for most games while giving still good quality results. This means they should work out of the box and be adapted to most games, including large maps. 
  You can adapt the quality of shadows, intensity of the light (and so shadows), the size of the rendered area by editing the Effects of the layer in the scene (as for other effects).
2025-07-02 15:49:26 +02:00
github-actions[bot]
5fe46ea8ea Update extension translations [skip ci] (#7682)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-07-02 11:50:59 +02:00
Clément Pasteau
4a590adac4 Add new marketing specialization course (#7684) 2025-07-02 11:50:27 +02:00
Clément Pasteau
279d41cdb7 Fix resource selector text color on light theme (#7681) 2025-07-01 14:58:50 +02:00
github-actions[bot]
5cf65a9f62 Update translations [skip ci] (#7675)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-07-01 14:58:24 +02:00
D8H
08b05c13b6 Fix a crash when using the "Fixed rotation" action on a 3D character (#7680) 2025-06-30 15:24:14 +02:00
Aurélien Vivet
eb55c85f4e Fix "Wheel offset Z" and "Front wheel drive" properties of the 3D car behavior (#7678) 2025-06-30 15:23:41 +02:00
Florian Rival
8a243440db Improve some mathematical tools descriptions 2025-06-29 14:25:36 +02:00
Florian Rival
b3e4e6b89c Fix missing MassCenterZ expression for the 3D physics behavior 2025-06-28 16:35:37 +02:00
Florian Rival
a1a25f6df4 Bump newIDE version 2025-06-26 13:46:07 +02:00
Aurélien Vivet
6114a6cec1 Update the Create action with information about object picking (#7673) 2025-06-26 12:32:20 +02:00
github-actions[bot]
5058964937 Update translations [skip ci] (#7672)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-06-26 12:32:02 +02:00
Florian Rival
4488675540 Fix tutorials not always showing the element to scroll to for behaviors or extensions 2025-06-26 11:25:38 +02:00
D8H
6a2d2c9e67 Hide the behavior update dialog during in-app tutorial (#7674) 2025-06-26 11:02:55 +02:00
Florian Rival
b43c42d763 Fix broken tutorials 2025-06-25 18:50:58 +02:00
github-actions[bot]
69112183d4 Update translations [skip ci] (#7665)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-06-24 18:50:23 +02:00
257 changed files with 21440 additions and 2848 deletions

View File

@@ -37,8 +37,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.SetIcon("res/actions/position24_black.png");
extension.AddInstructionOrExpressionGroupMetadata(_("Angle"))
.SetIcon("res/actions/direction24_black.png");
extension.AddInstructionOrExpressionGroupMetadata(_("Size"))
.SetIcon("res/actions/scale24_black.png");
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
"res/actions/scale24_black.png");
gd::ObjectMetadata& obj = extension.AddObject<gd::ObjectConfiguration>(
"", _("Base object"), _("Base object"), "res/objeticon24.png");
@@ -838,14 +838,13 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.MarkAsAdvanced()
.SetRelevantForLayoutEventsOnly();
obj.AddAction(
"PushBooleanToObjectVariable",
_("Add value to object array variable"),
_("Adds a boolean to the end of an object array variable."),
_("Add value _PARAM2_ to array variable _PARAM1_ of _PARAM0_"),
_("Variables Arrays and structures"),
"res/actions/var24.png",
"res/actions/var.png")
obj.AddAction("PushBooleanToObjectVariable",
_("Add value to object array variable"),
_("Adds a boolean to the end of an object array variable."),
_("Add value _PARAM2_ to array variable _PARAM1_ of _PARAM0_"),
_("Variables Arrays and structures"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("object", _("Object"))
.AddParameter("objectvar", _("Array variable"))
.AddParameter("trueorfalse", _("Boolean to add"))
@@ -1575,7 +1574,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
extension
.AddAction("Create",
_("Create an object"),
_("Create an object at specified position"),
_("Create an instance of the object at the specified position."
"The created object instance will be available for the next "
"actions and sub-events."),
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_ "
"(layer: _PARAM4_)"),
"",

View File

@@ -72,7 +72,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
extension
.AddExpression("normalize",
_("Normalize a value between `min` and `max` to a value between 0 and 1."),
_("Normalize a value between `min` and `max` to a value "
"between 0 and 1."),
_("Remap a value between 0 and 1."),
"",
"res/mathfunction.png")
@@ -124,7 +125,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
extension
.AddExpression("mod",
_("Modulo"),
_("x mod y"),
_("Compute \"x mod y\". GDevelop does NOT support the \% "
"operator. Use this mod(x, y) function instead."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("x (as in x mod y)"))
@@ -184,11 +186,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("asinh",
_("Arcsine"),
_("Arcsine"),
"",
"res/mathfunction.png")
.AddExpression(
"asinh", _("Arcsine"), _("Arcsine"), "", "res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -218,11 +217,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("cbrt",
_("Cube root"),
_("Cube root"),
"",
"res/mathfunction.png")
.AddExpression(
"cbrt", _("Cube root"), _("Cube root"), "", "res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -260,12 +256,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"), "", true);
extension
.AddExpression("cos",
_("Cosine"),
_("Cosine of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddExpression(
"cos",
_("Cosine"),
_("Cosine of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -293,29 +290,20 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("int",
_("Round"),
_("Round a number"),
"",
"res/mathfunction.png")
.AddExpression(
"int", _("Round"), _("Round a number"), "", "res/mathfunction.png")
.SetHidden()
.AddParameter("expression", _("Expression"));
extension
.AddExpression("rint",
_("Round"),
_("Round a number"),
"",
"res/mathfunction.png")
.AddExpression(
"rint", _("Round"), _("Round a number"), "", "res/mathfunction.png")
.SetHidden()
.AddParameter("expression", _("Expression"));
extension
.AddExpression("round",
_("Round"),
_("Round a number"),
"",
"res/mathfunction.png")
.AddExpression(
"round", _("Round"), _("Round a number"), "", "res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -324,8 +312,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
_("Round a number to the Nth decimal place"),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"))
.AddParameter("expression", _("Expression"), "", true);
.AddParameter("expression", _("Number to Round"))
.AddParameter("expression", _("Decimal Places"), "", true);
extension
.AddExpression("exp",
@@ -336,19 +324,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("log",
_("Logarithm"),
_("Logarithm"),
"",
"res/mathfunction.png")
.AddExpression(
"log", _("Logarithm"), _("Logarithm"), "", "res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
.AddExpression("ln",
_("Logarithm"),
_("Logarithm"),
"",
"res/mathfunction.png")
.AddExpression(
"ln", _("Logarithm"), _("Logarithm"), "", "res/mathfunction.png")
.SetHidden()
.AddParameter("expression", _("Expression"));
@@ -387,11 +369,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("The exponent (n in x^n)"));
extension
.AddExpression("sec",
_("Secant"),
_("Secant"),
"",
"res/mathfunction.png")
.AddExpression(
"sec", _("Secant"), _("Secant"), "", "res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -403,12 +382,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("sin",
_("Sine"),
_("Sine of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddExpression(
"sin",
_("Sine"),
_("Sine of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `sin(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -428,12 +408,13 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("Expression"));
extension
.AddExpression("tan",
_("Tangent"),
_("Tangent of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `tan(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddExpression(
"tan",
_("Tangent"),
_("Tangent of an angle (in radian). "
"If you want to use degrees, use`ToRad`: `tan(ToRad(45))`."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Expression"));
extension
@@ -463,26 +444,28 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
.AddParameter("expression", _("x (in a+(b-a)*x)"));
extension
.AddExpression("XFromAngleAndDistance",
_("X position from angle and distance"),
_("Compute the X position when given an angle and distance "
"relative to the origin (0;0). This is also known as "
"getting the cartesian coordinates of a 2D vector, using "
"its polar coordinates."),
"",
"res/mathfunction.png")
.AddExpression(
"XFromAngleAndDistance",
_("X position from angle and distance"),
_("Compute the X position when given an angle and distance "
"relative to the origin (0;0). This is also known as "
"getting the cartesian coordinates of a 2D vector, using "
"its polar coordinates."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Angle, in degrees"))
.AddParameter("expression", _("Distance"));
extension
.AddExpression("YFromAngleAndDistance",
_("Y position from angle and distance"),
_("Compute the Y position when given an angle and distance "
"relative to the origin (0;0). This is also known as "
"getting the cartesian coordinates of a 2D vector, using "
"its polar coordinates."),
"",
"res/mathfunction.png")
.AddExpression(
"YFromAngleAndDistance",
_("Y position from angle and distance"),
_("Compute the Y position when given an angle and distance "
"relative to the origin (0;0). This is also known as "
"getting the cartesian coordinates of a 2D vector, using "
"its polar coordinates."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Angle, in degrees"))
.AddParameter("expression", _("Distance"));
@@ -497,7 +480,8 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
extension
.AddExpression("lerpAngle",
_("Lerp (Linear interpolation) between two angles"),
_("Linearly interpolates between two angles (in degrees) by taking the shortest direction around the circle."),
_("Linearly interpolates between two angles (in degrees) "
"by taking the shortest direction around the circle."),
"",
"res/mathfunction.png")
.AddParameter("expression", _("Starting angle, in degrees"))

View File

@@ -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 GDCORE_EFFECT_H
#define GDCORE_EFFECT_H
#pragma once
#include <map>
namespace gd {
class SerializerElement;
@@ -43,6 +42,10 @@ class GD_CORE_API Effect {
return doubleParameters[name];
}
bool HasDoubleParameter(const gd::String& name) {
return doubleParameters.find(name) != doubleParameters.end();
}
void SetStringParameter(const gd::String& name, const gd::String& value) {
stringParameters[name] = value;
}
@@ -51,6 +54,10 @@ class GD_CORE_API Effect {
return stringParameters[name];
}
bool HasStringParameter(const gd::String& name) {
return stringParameters.find(name) != stringParameters.end();
}
void SetBooleanParameter(const gd::String& name, bool value) {
booleanParameters[name] = value;
}
@@ -59,6 +66,10 @@ class GD_CORE_API Effect {
return booleanParameters[name];
}
bool HasBooleanParameter(const gd::String& name) {
return booleanParameters.find(name) != booleanParameters.end();
}
const std::map<gd::String, double>& GetAllDoubleParameters() const {
return doubleParameters;
}
@@ -97,4 +108,3 @@ class GD_CORE_API Effect {
};
} // namespace gd
#endif

View File

@@ -36,7 +36,7 @@ namespace gd {
gd::BehaviorsSharedData Layout::badBehaviorSharedData("", "");
Layout::Layout(const Layout &other)
Layout::Layout(const Layout& other)
: objectsContainer(gd::ObjectsContainer::SourceType::Scene) {
Init(other);
}
@@ -54,6 +54,8 @@ Layout::Layout()
backgroundColorG(209),
backgroundColorB(209),
stopSoundsOnStartup(true),
resourcesPreloading("inherit"),
resourcesUnloading("inherit"),
standardSortMethod(true),
disableInputWhenNotFocused(true),
variables(gd::VariablesContainer::SourceType::Scene),
@@ -244,6 +246,10 @@ void Layout::SerializeTo(SerializerElement& element) const {
element.SetAttribute("title", GetWindowDefaultTitle());
element.SetAttribute("standardSortMethod", standardSortMethod);
element.SetAttribute("stopSoundsOnStartup", stopSoundsOnStartup);
if (resourcesPreloading != "inherit")
element.SetAttribute("resourcesPreloading", resourcesPreloading);
if (resourcesUnloading != "inherit")
element.SetAttribute("resourcesUnloading", resourcesUnloading);
element.SetAttribute("disableInputWhenNotFocused",
disableInputWhenNotFocused);
@@ -304,6 +310,10 @@ void Layout::UnserializeFrom(gd::Project& project,
element.GetStringAttribute("title", "(No title)", "titre"));
standardSortMethod = element.GetBoolAttribute("standardSortMethod");
stopSoundsOnStartup = element.GetBoolAttribute("stopSoundsOnStartup");
resourcesPreloading =
element.GetStringAttribute("resourcesPreloading", "inherit");
resourcesUnloading =
element.GetStringAttribute("resourcesUnloading", "inherit");
disableInputWhenNotFocused =
element.GetBoolAttribute("disableInputWhenNotFocused");
@@ -391,6 +401,8 @@ void Layout::Init(const Layout& other) {
standardSortMethod = other.standardSortMethod;
title = other.title;
stopSoundsOnStartup = other.stopSoundsOnStartup;
resourcesPreloading = other.resourcesPreloading;
resourcesUnloading = other.resourcesUnloading;
disableInputWhenNotFocused = other.disableInputWhenNotFocused;
initialInstances = other.initialInstances;
layers = other.layers;

View File

@@ -349,6 +349,36 @@ class GD_CORE_API Layout {
* launched
*/
bool StopSoundsOnStartup() const { return stopSoundsOnStartup; }
/**
* Set when the scene must preload its resources: `at-startup`, `never` or
* `inherit` (default).
*/
void SetResourcesPreloading(gd::String resourcesPreloading_) {
resourcesPreloading = resourcesPreloading_;
}
/**
* Get when the scene must preload its resources: `at-startup`, `never` or
* `inherit` (default).
*/
const gd::String& GetResourcesPreloading() const {
return resourcesPreloading;
}
/**
* Set when the scene must unload its resources: `at-scene-exit`, `never` or
* `inherit` (default).
*/
void SetResourcesUnloading(gd::String resourcesUnloading_) {
resourcesUnloading = resourcesUnloading_;
}
/**
* Get when the scene must unload its resources: `at-scene-exit`, `never` or
* `inherit` (default).
*/
const gd::String& GetResourcesUnloading() const { return resourcesUnloading; }
///@}
/** \name Saving and loading
@@ -381,6 +411,10 @@ class GD_CORE_API Layout {
behaviorsSharedData; ///< Initial shared datas of behaviors
bool stopSoundsOnStartup = true; ///< True to make the scene stop all sounds at
///< startup.
gd::String
resourcesPreloading; ///< `at-startup`, `never` or `inherit` (default).
gd::String
resourcesUnloading; ///< `at-scene-exit`, `never` or `inherit` (default).
bool standardSortMethod = true; ///< True to sort objects using standard sort.
bool disableInputWhenNotFocused = true; /// If set to true, the input must be
/// disabled when the window do not have the

View File

@@ -74,7 +74,9 @@ Project::Project()
gdMinorVersion(gd::VersionWrapper::Minor()),
gdBuildVersion(gd::VersionWrapper::Build()),
variables(gd::VariablesContainer::SourceType::Global),
objectsContainer(gd::ObjectsContainer::SourceType::Global) {}
objectsContainer(gd::ObjectsContainer::SourceType::Global),
sceneResourcesPreloading("at-startup"),
sceneResourcesUnloading("never") {}
Project::~Project() {}
@@ -1166,6 +1168,13 @@ void Project::SerializeTo(SerializerElement& element) const {
else
std::cout << "ERROR: The project current platform is NULL.";
if (sceneResourcesPreloading != "at-startup") {
propElement.SetAttribute("sceneResourcesPreloading", sceneResourcesPreloading);
}
if (sceneResourcesUnloading != "never") {
propElement.SetAttribute("sceneResourcesUnloading", sceneResourcesUnloading);
}
resourcesManager.SerializeTo(element.AddChild("resources"));
objectsContainer.SerializeObjectsTo(element.AddChild("objects"));
objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure"));
@@ -1307,6 +1316,9 @@ void Project::Init(const gd::Project& game) {
variables = game.GetVariables();
projectFile = game.GetProjectFile();
sceneResourcesPreloading = game.sceneResourcesPreloading;
sceneResourcesUnloading = game.sceneResourcesUnloading;
}
} // namespace gd

View File

@@ -964,6 +964,37 @@ class GD_CORE_API Project {
*/
ResourcesManager& GetResourcesManager() { return resourcesManager; }
/**
* Set when the scenes must preload their resources: `at-startup`, `never`
* (default).
*/
void SetSceneResourcesPreloading(gd::String sceneResourcesPreloading_) {
sceneResourcesPreloading = sceneResourcesPreloading_;
}
/**
* Get when the scenes must preload their resources: `at-startup`, `never`
* (default).
*/
const gd::String& GetSceneResourcesPreloading() const {
return sceneResourcesPreloading;
}
/**
* Set when the scenes must unload their resources: `at-scene-exit`, `never`
* (default).
*/
void SetSceneResourcesUnloading(gd::String sceneResourcesUnloading_) {
sceneResourcesUnloading = sceneResourcesUnloading_;
}
/**
* Get when the scenes must unload their resources: `at-scene-exit`, `never`
* (default).
*/
const gd::String& GetSceneResourcesUnloading() const {
return sceneResourcesUnloading;
}
///@}
/** \name Variable management
@@ -1121,6 +1152,10 @@ class GD_CORE_API Project {
ExtensionProperties
extensionProperties; ///< The properties of the extensions.
gd::WholeProjectDiagnosticReport wholeProjectDiagnosticReport;
gd::String sceneResourcesPreloading; ///< `at-startup` or `never`
///< (default: `at-startup`).
gd::String sceneResourcesUnloading; ///< `at-scene-exit` or `never`
///< (default: `never`).
mutable unsigned int gdMajorVersion =
0; ///< The GD major version used the last
///< time the project was saved.

View File

@@ -7,9 +7,9 @@
#define GDCORE_PROPERTYDESCRIPTOR
#include <vector>
#include "GDCore/String.h"
#include "GDCore/Project/MeasurementUnit.h"
#include "GDCore/Project/QuickCustomization.h"
#include "GDCore/String.h"
namespace gd {
class SerializerElement;
@@ -17,6 +17,19 @@ class SerializerElement;
namespace gd {
class GD_CORE_API PropertyDescriptorChoice {
public:
PropertyDescriptorChoice(const gd::String& value, const gd::String& label)
: value(value), label(label) {}
const gd::String& GetValue() const { return value; }
const gd::String& GetLabel() const { return label; }
private:
gd::String value;
gd::String label;
};
/**
* \brief Used to describe a property shown in a property grid.
* \see gd::Object
@@ -31,8 +44,12 @@ class GD_CORE_API PropertyDescriptor {
* \param propertyValue The value of the property.
*/
PropertyDescriptor(gd::String propertyValue)
: currentValue(propertyValue), type("string"), label(""), hidden(false),
deprecated(false), advanced(false),
: currentValue(propertyValue),
type("string"),
label(""),
hidden(false),
deprecated(false),
advanced(false),
hasImpactOnOtherProperties(false),
measurementUnit(gd::MeasurementUnit::GetUndefined()),
quickCustomizationVisibility(QuickCustomization::Visibility::Default) {}
@@ -41,10 +58,13 @@ class GD_CORE_API PropertyDescriptor {
* \brief Empty constructor creating an empty property to be displayed.
*/
PropertyDescriptor()
: hidden(false), deprecated(false), advanced(false),
: hidden(false),
deprecated(false),
advanced(false),
hasImpactOnOtherProperties(false),
measurementUnit(gd::MeasurementUnit::GetUndefined()),
quickCustomizationVisibility(QuickCustomization::Visibility::Default){};
quickCustomizationVisibility(QuickCustomization::Visibility::Default) {
};
/**
* \brief Destructor
@@ -88,13 +108,20 @@ class GD_CORE_API PropertyDescriptor {
}
/**
* \brief Change the group where this property is displayed to the user, if any.
* \brief Change the group where this property is displayed to the user, if
* any.
*/
PropertyDescriptor& SetGroup(gd::String group_) {
group = group_;
return *this;
}
PropertyDescriptor& AddChoice(const gd::String& value,
const gd::String& label) {
choices.push_back(PropertyDescriptorChoice(value, label));
return *this;
}
/**
* \brief Set and replace the additional information for the property.
*/
@@ -118,7 +145,8 @@ class GD_CORE_API PropertyDescriptor {
/**
* \brief Change the unit of measurement of the property value.
*/
PropertyDescriptor& SetMeasurementUnit(const gd::MeasurementUnit &measurementUnit_) {
PropertyDescriptor& SetMeasurementUnit(
const gd::MeasurementUnit& measurementUnit_) {
measurementUnit = measurementUnit_;
return *this;
}
@@ -128,14 +156,18 @@ class GD_CORE_API PropertyDescriptor {
const gd::String& GetLabel() const { return label; }
const gd::String& GetDescription() const { return description; }
const gd::String& GetGroup() const { return group; }
const gd::MeasurementUnit& GetMeasurementUnit() const { return measurementUnit; }
const gd::MeasurementUnit& GetMeasurementUnit() const {
return measurementUnit;
}
const std::vector<gd::String>& GetExtraInfo() const {
return extraInformation;
}
std::vector<gd::String>& GetExtraInfo() {
return extraInformation;
std::vector<gd::String>& GetExtraInfo() { return extraInformation; }
const std::vector<PropertyDescriptorChoice>& GetChoices() const {
return choices;
}
/**
@@ -178,23 +210,26 @@ class GD_CORE_API PropertyDescriptor {
bool IsAdvanced() const { return advanced; }
/**
* \brief Check if the property has impact on other properties - which means a change
* must re-render other properties.
* \brief Check if the property has impact on other properties - which means a
* change must re-render other properties.
*/
bool HasImpactOnOtherProperties() const { return hasImpactOnOtherProperties; }
/**
* \brief Set if the property has impact on other properties - which means a change
* must re-render other properties.
* \brief Set if the property has impact on other properties - which means a
* change must re-render other properties.
*/
PropertyDescriptor& SetHasImpactOnOtherProperties(bool enable) {
hasImpactOnOtherProperties = enable;
return *this;
}
QuickCustomization::Visibility GetQuickCustomizationVisibility() const { return quickCustomizationVisibility; }
QuickCustomization::Visibility GetQuickCustomizationVisibility() const {
return quickCustomizationVisibility;
}
PropertyDescriptor& SetQuickCustomizationVisibility(QuickCustomization::Visibility visibility) {
PropertyDescriptor& SetQuickCustomizationVisibility(
QuickCustomization::Visibility visibility) {
quickCustomizationVisibility = visibility;
return *this;
}
@@ -231,15 +266,17 @@ class GD_CORE_API PropertyDescriptor {
gd::String label; //< The user-friendly property name
gd::String description; //< The user-friendly property description
gd::String group; //< The user-friendly property group
std::vector<PropertyDescriptorChoice>
choices; //< The optional choices for the property.
std::vector<gd::String>
extraInformation; ///< Can be used to store for example the available
///< choices, if a property is a displayed as a combo
///< box.
extraInformation; ///< Can be used to store an additional information
///< like an object type.
bool hidden;
bool deprecated;
bool advanced;
bool hasImpactOnOtherProperties;
gd::MeasurementUnit measurementUnit; //< The unit of measurement of the property vale.
gd::MeasurementUnit
measurementUnit; //< The unit of measurement of the property vale.
QuickCustomization::Visibility quickCustomizationVisibility;
};

View File

@@ -25,6 +25,8 @@ namespace gdjs {
topFaceVisible: boolean;
bottomFaceVisible: boolean;
tint: string | undefined;
isCastingShadow: boolean;
isReceivingShadow: boolean;
materialType: 'Basic' | 'StandardWithoutMetalness';
};
}
@@ -71,8 +73,10 @@ namespace gdjs {
string,
];
_materialType: gdjs.Cube3DRuntimeObject.MaterialType =
gdjs.Cube3DRuntimeObject.MaterialType.Basic;
gdjs.Cube3DRuntimeObject.MaterialType.StandardWithoutMetalness;
_tint: string;
_isCastingShadow: boolean = true;
_isReceivingShadow: boolean = true;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -121,6 +125,8 @@ namespace gdjs {
];
this._tint = objectData.content.tint || '255;255;255';
this._isCastingShadow = objectData.content.isCastingShadow || false;
this._isReceivingShadow = objectData.content.isReceivingShadow || false;
this._materialType = this._convertMaterialType(
objectData.content.materialType
@@ -430,6 +436,18 @@ namespace gdjs {
) {
this.setMaterialType(newObjectData.content.materialType);
}
if (
oldObjectData.content.isCastingShadow !==
newObjectData.content.isCastingShadow
) {
this.updateShadowCasting(newObjectData.content.isCastingShadow);
}
if (
oldObjectData.content.isReceivingShadow !==
newObjectData.content.isReceivingShadow
) {
this.updateShadowReceiving(newObjectData.content.isReceivingShadow);
}
return true;
}
@@ -531,6 +549,14 @@ namespace gdjs {
this._materialType = newMaterialType;
this._renderer._updateMaterials();
}
updateShadowCasting(value: boolean) {
this._isCastingShadow = value;
this._renderer.updateShadowCasting();
}
updateShadowReceiving(value: boolean) {
this._isReceivingShadow = value;
this._renderer.updateShadowReceiving();
}
}
export namespace Cube3DRuntimeObject {

View File

@@ -81,13 +81,14 @@ namespace gdjs {
.map((_, index) =>
getFaceMaterial(runtimeObject, materialIndexToFaceIndex[index])
);
const boxMesh = new THREE.Mesh(geometry, materials);
super(runtimeObject, instanceContainer, boxMesh);
this._boxMesh = boxMesh;
this._cube3DRuntimeObject = runtimeObject;
boxMesh.receiveShadow = this._cube3DRuntimeObject._isReceivingShadow;
boxMesh.castShadow = this._cube3DRuntimeObject._isCastingShadow;
this.updateSize();
this.updatePosition();
this.updateRotation();
@@ -114,6 +115,13 @@ namespace gdjs {
new THREE.BufferAttribute(new Float32Array(tints), 3)
);
}
updateShadowCasting() {
this._boxMesh.castShadow = this._cube3DRuntimeObject._isCastingShadow;
}
updateShadowReceiving() {
this._boxMesh.receiveShadow =
this._cube3DRuntimeObject._isReceivingShadow;
}
updateFace(faceIndex: integer) {
const materialIndex = faceIndexToMaterialIndex[faceIndex];

View File

@@ -6,6 +6,7 @@ namespace gdjs {
r: number;
t: string;
}
const shadowHelper = false;
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::DirectionalLight',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
@@ -17,19 +18,63 @@ namespace gdjs {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
light: THREE.DirectionalLight;
rotationObject: THREE.Group;
_isEnabled: boolean = false;
top: string = 'Y-';
elevation: float = 45;
rotation: float = 0;
private _top: string = 'Z+';
private _elevation: float = 45;
private _rotation: float = 0;
private _shadowMapSize: float = 1024;
private _minimumShadowBias: float = 0;
private _distanceFromCamera: float = 1500;
private _frustumSize: float = 4000;
private _isEnabled: boolean = false;
private _light: THREE.DirectionalLight;
private _shadowMapDirty = true;
private _shadowCameraDirty = true;
private _shadowCameraHelper: THREE.CameraHelper | null;
constructor() {
this.light = new THREE.DirectionalLight();
this.light.position.set(1, 0, 0);
this.rotationObject = new THREE.Group();
this.rotationObject.add(this.light);
this.updateRotation();
this._light = new THREE.DirectionalLight();
if (shadowHelper) {
this._shadowCameraHelper = new THREE.CameraHelper(
this._light.shadow.camera
);
} else {
this._shadowCameraHelper = null;
}
this._light.shadow.camera.updateProjectionMatrix();
}
private _updateShadowCamera(): void {
if (!this._shadowCameraDirty) {
return;
}
this._shadowCameraDirty = false;
this._light.shadow.camera.near = 1;
this._light.shadow.camera.far = this._distanceFromCamera + 10000;
this._light.shadow.camera.right = this._frustumSize / 2;
this._light.shadow.camera.left = -this._frustumSize / 2;
this._light.shadow.camera.top = this._frustumSize / 2;
this._light.shadow.camera.bottom = -this._frustumSize / 2;
}
private _updateShadowMapSize(): void {
if (!this._shadowMapDirty) {
return;
}
this._shadowMapDirty = false;
this._light.shadow.mapSize.set(
this._shadowMapSize,
this._shadowMapSize
);
// Force the recreation of the shadow map texture:
this._light.shadow.map?.dispose();
this._light.shadow.map = null;
this._light.shadow.needsUpdate = true;
}
isEnabled(target: EffectsTarget): boolean {
@@ -53,7 +98,12 @@ namespace gdjs {
if (!scene) {
return false;
}
scene.add(this.rotationObject);
scene.add(this._light);
scene.add(this._light.target);
if (this._shadowCameraHelper) {
scene.add(this._shadowCameraHelper);
}
this._isEnabled = true;
return true;
}
@@ -65,82 +115,164 @@ namespace gdjs {
if (!scene) {
return false;
}
scene.remove(this.rotationObject);
scene.remove(this._light);
scene.remove(this._light.target);
if (this._shadowCameraHelper) {
scene.remove(this._shadowCameraHelper);
}
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updatePreRender(target: gdjs.EffectsTarget): any {
// Apply any update to the camera or shadow map size.
this._updateShadowCamera();
this._updateShadowMapSize();
// Avoid shadow acne due to depth buffer precision.
const biasMultiplier =
this._shadowMapSize < 1024
? 2
: this._shadowMapSize < 2048
? 1.25
: 1;
this._light.shadow.bias = -this._minimumShadowBias * biasMultiplier;
// Apply update to the light position and its target.
// By doing this, the shadows are "following" the GDevelop camera.
if (!target.getRuntimeLayer) {
return;
}
const layer = target.getRuntimeLayer();
const x = layer.getCameraX();
const y = layer.getCameraY();
const z = layer.getCameraZ(layer.getInitialCamera3DFieldOfView());
const roundedX = Math.floor(x / 100) * 100;
const roundedY = Math.floor(y / 100) * 100;
const roundedZ = Math.floor(z / 100) * 100;
if (this._top === 'Y-') {
const posLightX =
roundedX +
this._distanceFromCamera *
Math.cos(gdjs.toRad(-this._rotation + 90)) *
Math.cos(gdjs.toRad(this._elevation));
const posLightY =
roundedY -
this._distanceFromCamera *
Math.sin(gdjs.toRad(this._elevation));
const posLightZ =
roundedZ +
this._distanceFromCamera *
Math.sin(gdjs.toRad(-this._rotation + 90)) *
Math.cos(gdjs.toRad(this._elevation));
this._light.position.set(posLightX, posLightY, posLightZ);
this._light.target.position.set(roundedX, roundedY, roundedZ);
} else {
const posLightX =
roundedX +
this._distanceFromCamera *
Math.cos(gdjs.toRad(this._rotation)) *
Math.cos(gdjs.toRad(this._elevation));
const posLightY =
roundedY +
this._distanceFromCamera *
Math.sin(gdjs.toRad(this._rotation)) *
Math.cos(gdjs.toRad(this._elevation));
const posLightZ =
roundedZ +
this._distanceFromCamera *
Math.sin(gdjs.toRad(this._elevation));
this._light.position.set(posLightX, posLightY, posLightZ);
this._light.target.position.set(roundedX, roundedY, roundedZ);
}
}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'intensity') {
this.light.intensity = value;
this._light.intensity = value;
} else if (parameterName === 'elevation') {
this.elevation = value;
this.updateRotation();
this._elevation = value;
} else if (parameterName === 'rotation') {
this.rotation = value;
this.updateRotation();
this._rotation = value;
} else if (parameterName === 'distanceFromCamera') {
this._distanceFromCamera = value;
} else if (parameterName === 'frustumSize') {
this._frustumSize = value;
} else if (parameterName === 'minimumShadowBias') {
this._minimumShadowBias = value;
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'intensity') {
return this.light.intensity;
return this._light.intensity;
} else if (parameterName === 'elevation') {
return this.elevation;
return this._elevation;
} else if (parameterName === 'rotation') {
return this.rotation;
return this._rotation;
} else if (parameterName === 'distanceFromCamera') {
return this._distanceFromCamera;
} else if (parameterName === 'frustumSize') {
return this._frustumSize;
} else if (parameterName === 'minimumShadowBias') {
return this._minimumShadowBias;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {
if (parameterName === 'color') {
this.light.color = new THREE.Color(
this._light.color = new THREE.Color(
gdjs.rgbOrHexStringToNumber(value)
);
}
if (parameterName === 'top') {
this.top = value;
this.updateRotation();
this._top = value;
}
if (parameterName === 'shadowQuality') {
if (value === 'low' && this._shadowMapSize !== 512) {
this._shadowMapSize = 512;
this._shadowMapDirty = true;
}
if (value === 'medium' && this._shadowMapSize !== 1024) {
this._shadowMapSize = 1024;
this._shadowMapDirty = true;
}
if (value === 'high' && this._shadowMapSize !== 2048) {
this._shadowMapSize = 2048;
this._shadowMapDirty = true;
}
}
}
updateColorParameter(parameterName: string, value: number): void {
if (parameterName === 'color') {
this.light.color.setHex(value);
this._light.color.setHex(value);
}
}
getColorParameter(parameterName: string): number {
if (parameterName === 'color') {
return this.light.color.getHex();
return this._light.color.getHex();
}
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
updateRotation() {
if (this.top === 'Z+') {
// 0° is a light from the right of the screen.
this.rotationObject.rotation.z = gdjs.toRad(this.rotation);
this.rotationObject.rotation.y = -gdjs.toRad(this.elevation);
} else {
// 0° becomes a light from Z+.
this.rotationObject.rotation.y = gdjs.toRad(this.rotation - 90);
this.rotationObject.rotation.z = -gdjs.toRad(this.elevation);
updateBooleanParameter(parameterName: string, value: boolean): void {
if (parameterName === 'isCastingShadow') {
this._light.castShadow = value;
}
}
getNetworkSyncData(): DirectionalLightFilterNetworkSyncData {
return {
i: this.light.intensity,
c: this.light.color.getHex(),
e: this.elevation,
r: this.rotation,
t: this.top,
i: this._light.intensity,
c: this._light.color.getHex(),
e: this._elevation,
r: this._rotation,
t: this._top,
};
}
updateFromNetworkSyncData(syncData: any): void {
this.light.intensity = syncData.i;
this.light.color.setHex(syncData.c);
this.elevation = syncData.e;
this.rotation = syncData.r;
this.top = syncData.t;
this.updateRotation();
this._light.intensity = syncData.i;
this._light.color.setHex(syncData.c);
this._elevation = syncData.e;
this._rotation = syncData.r;
this._top = syncData.t;
}
})();
}

View File

@@ -18,18 +18,15 @@ namespace gdjs {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
light: THREE.HemisphereLight;
rotationObject: THREE.Group;
_top: string = 'Z+';
_elevation: float = 90;
_rotation: float = 0;
_isEnabled: boolean = false;
top: string = 'Y-';
elevation: float = 45;
rotation: float = 0;
_light: THREE.HemisphereLight;
constructor() {
this.light = new THREE.HemisphereLight();
this.light.position.set(1, 0, 0);
this.rotationObject = new THREE.Group();
this.rotationObject.add(this.light);
this._light = new THREE.HemisphereLight();
this.updateRotation();
}
@@ -54,7 +51,7 @@ namespace gdjs {
if (!scene) {
return false;
}
scene.add(this.rotationObject);
scene.add(this._light);
this._isEnabled = true;
return true;
}
@@ -66,96 +63,106 @@ namespace gdjs {
if (!scene) {
return false;
}
scene.remove(this.rotationObject);
scene.remove(this._light);
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {
if (parameterName === 'intensity') {
this.light.intensity = value;
this._light.intensity = value;
} else if (parameterName === 'elevation') {
this.elevation = value;
this._elevation = value;
this.updateRotation();
} else if (parameterName === 'rotation') {
this.rotation = value;
this._rotation = value;
this.updateRotation();
}
}
getDoubleParameter(parameterName: string): number {
if (parameterName === 'intensity') {
return this.light.intensity;
return this._light.intensity;
} else if (parameterName === 'elevation') {
return this.elevation;
return this._elevation;
} else if (parameterName === 'rotation') {
return this.rotation;
return this._rotation;
}
return 0;
}
updateStringParameter(parameterName: string, value: string): void {
if (parameterName === 'skyColor') {
this.light.color = new THREE.Color(
this._light.color = new THREE.Color(
gdjs.rgbOrHexStringToNumber(value)
);
}
if (parameterName === 'groundColor') {
this.light.groundColor = new THREE.Color(
this._light.groundColor = new THREE.Color(
gdjs.rgbOrHexStringToNumber(value)
);
}
if (parameterName === 'top') {
this.top = value;
this._top = value;
this.updateRotation();
}
}
updateColorParameter(parameterName: string, value: number): void {
if (parameterName === 'skyColor') {
this.light.color.setHex(value);
this._light.color.setHex(value);
}
if (parameterName === 'groundColor') {
this.light.groundColor.setHex(value);
this._light.groundColor.setHex(value);
}
}
getColorParameter(parameterName: string): number {
if (parameterName === 'skyColor') {
return this.light.color.getHex();
return this._light.color.getHex();
}
if (parameterName === 'groundColor') {
return this.light.groundColor.getHex();
return this._light.groundColor.getHex();
}
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
updateRotation() {
if (this.top === 'Z+') {
// 0° is a light from the right of the screen.
this.rotationObject.rotation.z = gdjs.toRad(this.rotation);
this.rotationObject.rotation.y = -gdjs.toRad(this.elevation);
if (this._top === 'Y-') {
// `rotation` at 0° becomes a light from Z+.
this._light.position.set(
Math.cos(gdjs.toRad(-this._rotation + 90)) *
Math.cos(gdjs.toRad(this._elevation)),
-Math.sin(gdjs.toRad(this._elevation)),
Math.sin(gdjs.toRad(-this._rotation + 90)) *
Math.cos(gdjs.toRad(this._elevation))
);
} else {
// 0° becomes a light from Z+.
this.rotationObject.rotation.y = gdjs.toRad(this.rotation - 90);
this.rotationObject.rotation.z = -gdjs.toRad(this.elevation);
// `rotation` at 0° is a light from the right of the screen.
this._light.position.set(
Math.cos(gdjs.toRad(this._rotation)) *
Math.cos(gdjs.toRad(this._elevation)),
Math.sin(gdjs.toRad(this._rotation)) *
Math.cos(gdjs.toRad(this._elevation)),
Math.sin(gdjs.toRad(this._elevation))
);
}
}
getNetworkSyncData(): HemisphereLightFilterNetworkSyncData {
return {
i: this.light.intensity,
sc: this.light.color.getHex(),
gc: this.light.groundColor.getHex(),
e: this.elevation,
r: this.rotation,
t: this.top,
i: this._light.intensity,
sc: this._light.color.getHex(),
gc: this._light.groundColor.getHex(),
e: this._elevation,
r: this._rotation,
t: this._top,
};
}
updateFromNetworkSyncData(
syncData: HemisphereLightFilterNetworkSyncData
): void {
this.light.intensity = syncData.i;
this.light.color.setHex(syncData.sc);
this.light.groundColor.setHex(syncData.gc);
this.elevation = syncData.e;
this.rotation = syncData.r;
this.top = syncData.t;
this._light.intensity = syncData.i;
this._light.color.setHex(syncData.sc);
this._light.groundColor.setHex(syncData.gc);
this._elevation = syncData.e;
this._rotation = syncData.r;
this._top = syncData.t;
this.updateRotation();
}
})();

View File

@@ -859,7 +859,9 @@ module.exports = {
propertyName === 'rightFaceResourceRepeat' ||
propertyName === 'topFaceResourceRepeat' ||
propertyName === 'bottomFaceResourceRepeat' ||
propertyName === 'enableTextureTransparency'
propertyName === 'enableTextureTransparency' ||
propertyName === 'isCastingShadow' ||
propertyName === 'isReceivingShadow'
) {
objectContent[propertyName] = newValue === '1';
return true;
@@ -887,8 +889,8 @@ module.exports = {
.getOrCreate('facesOrientation')
.setValue(objectContent.facesOrientation || 'Y')
.setType('choice')
.addExtraInfo('Y')
.addExtraInfo('Z')
.addChoice('Y', 'Y')
.addChoice('Z', 'Z')
.setLabel(_('Faces orientation'))
.setDescription(
_(
@@ -948,8 +950,8 @@ module.exports = {
.getOrCreate('backFaceUpThroughWhichAxisRotation')
.setValue(objectContent.backFaceUpThroughWhichAxisRotation || 'X')
.setType('choice')
.addExtraInfo('X')
.addExtraInfo('Y')
.addChoice('X', 'X')
.addChoice('Y', 'Y')
.setLabel(_('Back face orientation'))
.setDescription(
_(
@@ -1083,11 +1085,29 @@ module.exports = {
objectProperties
.getOrCreate('materialType')
.setValue(objectContent.materialType || 'Basic')
.setValue(objectContent.materialType || 'StandardWithoutMetalness')
.setType('choice')
.addExtraInfo('Basic')
.addExtraInfo('StandardWithoutMetalness')
.setLabel(_('Material type'));
.addChoice('Basic', _('Basic (no lighting, no shadows)'))
.addChoice(
'StandardWithoutMetalness',
_('Standard (without metalness)')
)
.setLabel(_('Material type'))
.setGroup(_('Lighting'));
objectProperties
.getOrCreate('isCastingShadow')
.setValue(objectContent.isCastingShadow ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Shadow casting'))
.setGroup(_('Lighting'));
objectProperties
.getOrCreate('isReceivingShadow')
.setValue(objectContent.isReceivingShadow ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Shadow receiving'))
.setGroup(_('Lighting'));
return objectProperties;
};
@@ -1116,8 +1136,10 @@ module.exports = {
rightFaceResourceRepeat: false,
topFaceResourceRepeat: false,
bottomFaceResourceRepeat: false,
materialType: 'Basic',
materialType: 'StandardWithoutMetalness',
tint: '255;255;255',
isCastingShadow: true,
isReceivingShadow: true,
};
Cube3DObject.updateInitialInstanceProperty = function (
@@ -1894,11 +1916,11 @@ module.exports = {
.setType('number');
properties
.getOrCreate('top')
.setValue('Y-')
.setValue('Z+')
.setLabel(_('3D world top'))
.setType('choice')
.addExtraInfo('Y-')
.addExtraInfo('Z+')
.addExtraInfo('Y-')
.setGroup(_('Orientation'));
properties
.getOrCreate('elevation')
@@ -1913,6 +1935,47 @@ module.exports = {
.setLabel(_('Rotation (in degrees)'))
.setType('number')
.setGroup(_('Orientation'));
properties
.getOrCreate('isCastingShadow')
.setValue('false')
.setLabel(_('Shadow casting'))
.setType('boolean')
.setGroup(_('Shadows'));
properties
.getOrCreate('shadowQuality')
.setValue('medium')
.addChoice('low', _('Low quality'))
.addChoice('medium', _('Medium quality'))
.addChoice('high', _('High quality'))
.setLabel(_('Shadow quality'))
.setType('choice')
.setGroup(_('Shadows'));
properties
.getOrCreate('minimumShadowBias')
.setValue('0')
.setLabel(_('Shadow bias'))
.setDescription(
_(
'Use this to avoid "shadow acne" due to depth buffer precision. Choose a value small enough like 0.001 to avoid creating distance between shadows and objects but not too small to avoid shadow glitches on low/medium quality. This value is used for high quality, and multiplied by 1.25 for medium quality and 2 for low quality.'
)
)
.setType('number')
.setGroup(_('Shadows'))
.setAdvanced(true);
properties
.getOrCreate('frustumSize')
.setValue('4000')
.setLabel(_('Shadow frustum size'))
.setType('number')
.setGroup(_('Shadows'))
.setAdvanced(true);
properties
.getOrCreate('distanceFromCamera')
.setValue('1500')
.setLabel(_("Distance from layer's camera"))
.setType('number')
.setGroup(_('Shadows'))
.setAdvanced(true);
}
{
const effect = extension
@@ -1944,11 +2007,11 @@ module.exports = {
.setType('number');
properties
.getOrCreate('top')
.setValue('Y-')
.setValue('Z+')
.setLabel(_('3D world top'))
.setType('choice')
.addExtraInfo('Y-')
.addExtraInfo('Z+')
.addExtraInfo('Y-')
.setGroup(_('Orientation'));
properties
.getOrCreate('elevation')
@@ -3210,6 +3273,8 @@ module.exports = {
this._threeObject = new THREE.Group();
this._threeObject.rotation.order = 'ZYX';
this._threeObject.castShadow = true;
this._threeObject.receiveShadow = true;
this._threeGroup.add(this._threeObject);
}

View File

@@ -23,7 +23,7 @@ Model3DObjectConfiguration::Model3DObjectConfiguration()
: width(100), height(100), depth(100), rotationX(0), rotationY(0),
rotationZ(0), modelResourceName(""), materialType("StandardWithoutMetalness"),
originLocation("ModelOrigin"), centerLocation("ModelOrigin"),
keepAspectRatio(true), crossfadeDuration(0.1f) {}
keepAspectRatio(true), crossfadeDuration(0.1f), isCastingShadow(true), isReceivingShadow(true) {}
bool Model3DObjectConfiguration::UpdateProperty(const gd::String &propertyName,
const gd::String &newValue) {
@@ -75,6 +75,16 @@ bool Model3DObjectConfiguration::UpdateProperty(const gd::String &propertyName,
crossfadeDuration = newValue.To<double>();
return true;
}
if(propertyName == "isCastingShadow")
{
isCastingShadow = newValue == "1";
return true;
}
if(propertyName == "isReceivingShadow")
{
isReceivingShadow = newValue == "1";
return true;
}
return false;
}
@@ -143,19 +153,20 @@ Model3DObjectConfiguration::GetProperties() const {
objectProperties["materialType"]
.SetValue(materialType.empty() ? "Basic" : materialType)
.SetType("choice")
.AddExtraInfo("Basic")
.AddExtraInfo("StandardWithoutMetalness")
.AddExtraInfo("KeepOriginal")
.SetLabel(_("Material"));
.AddChoice("Basic", _("Basic (no lighting, no shadows)"))
.AddChoice("StandardWithoutMetalness", _("Standard (without metalness)"))
.AddChoice("KeepOriginal", _("Keep original"))
.SetLabel(_("Material"))
.SetGroup(_("Lighting"));
objectProperties["originLocation"]
.SetValue(originLocation.empty() ? "TopLeft" : originLocation)
.SetType("choice")
.AddExtraInfo("ModelOrigin")
.AddExtraInfo("TopLeft")
.AddExtraInfo("ObjectCenter")
.AddExtraInfo("BottomCenterZ")
.AddExtraInfo("BottomCenterY")
.AddChoice("ModelOrigin", _("Model origin"))
.AddChoice("TopLeft", _("Top left"))
.AddChoice("ObjectCenter", _("Object center"))
.AddChoice("BottomCenterZ", _("Bottom center (Z)"))
.AddChoice("BottomCenterY", _("Bottom center (Y)"))
.SetLabel(_("Origin point"))
.SetGroup(_("Points"))
.SetAdvanced(true);
@@ -163,10 +174,10 @@ Model3DObjectConfiguration::GetProperties() const {
objectProperties["centerLocation"]
.SetValue(centerLocation.empty() ? "ObjectCenter" : centerLocation)
.SetType("choice")
.AddExtraInfo("ModelOrigin")
.AddExtraInfo("ObjectCenter")
.AddExtraInfo("BottomCenterZ")
.AddExtraInfo("BottomCenterY")
.AddChoice("ModelOrigin", _("Model origin"))
.AddChoice("ObjectCenter", _("Object center"))
.AddChoice("BottomCenterZ", _("Bottom center (Z)"))
.AddChoice("BottomCenterY", _("Bottom center (Y)"))
.SetLabel(_("Center point"))
.SetGroup(_("Points"))
.SetAdvanced(true);
@@ -178,6 +189,20 @@ Model3DObjectConfiguration::GetProperties() const {
.SetGroup(_("Animations"))
.SetMeasurementUnit(gd::MeasurementUnit::GetSecond());
objectProperties["isCastingShadow"]
.SetValue(isCastingShadow ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Shadow casting"))
.SetGroup(_("Lighting"));
objectProperties["isReceivingShadow"]
.SetValue(isReceivingShadow ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Shadow receiving"))
.SetGroup(_("Lighting"));
return objectProperties;
}
@@ -210,6 +235,8 @@ void Model3DObjectConfiguration::DoUnserializeFrom(
centerLocation = content.GetStringAttribute("centerLocation");
keepAspectRatio = content.GetBoolAttribute("keepAspectRatio");
crossfadeDuration = content.GetDoubleAttribute("crossfadeDuration");
isCastingShadow = content.GetBoolAttribute("isCastingShadow");
isReceivingShadow = content.GetBoolAttribute("isReceivingShadow");
RemoveAllAnimations();
auto &animationsElement = content.GetChild("animations");
@@ -239,6 +266,8 @@ void Model3DObjectConfiguration::DoSerializeTo(
content.SetAttribute("centerLocation", centerLocation);
content.SetAttribute("keepAspectRatio", keepAspectRatio);
content.SetAttribute("crossfadeDuration", crossfadeDuration);
content.SetAttribute("isCastingShadow", isCastingShadow);
content.SetAttribute("isReceivingShadow", isReceivingShadow);
auto &animationsElement = content.AddChild("animations");
animationsElement.ConsiderAsArrayOf("animation");

View File

@@ -160,6 +160,8 @@ public:
const gd::String& GetCenterLocation() const { return centerLocation; };
bool shouldKeepAspectRatio() const { return keepAspectRatio; };
bool shouldCastShadow() const { return isCastingShadow; };
bool shouldReceiveShadow() const { return isReceivingShadow; };
///@}
protected:
@@ -182,6 +184,8 @@ private:
gd::String centerLocation;
bool keepAspectRatio;
bool isCastingShadow;
bool isReceivingShadow;
std::vector<Model3DAnimation> animations;
static Model3DAnimation badAnimation; //< Bad animation when an out of bound

View File

@@ -38,6 +38,8 @@ namespace gdjs {
| 'BottomCenterY';
animations: Model3DAnimation[];
crossfadeDuration: float;
isCastingShadow: boolean;
isReceivingShadow: boolean;
};
}
@@ -101,6 +103,8 @@ namespace gdjs {
_animationSpeedScale: float = 1;
_animationPaused: boolean = false;
_crossfadeDuration: float = 0;
_isCastingShadow: boolean = true;
_isReceivingShadow: boolean = true;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -123,6 +127,8 @@ namespace gdjs {
objectData.content.materialType
);
this.setIsCastingShadow(objectData.content.isCastingShadow);
this.setIsReceivingShadow(objectData.content.isReceivingShadow);
this.onModelChanged(objectData);
this._crossfadeDuration = objectData.content.crossfadeDuration || 0;
@@ -195,6 +201,18 @@ namespace gdjs {
newObjectData.content.centerLocation
);
}
if (
oldObjectData.content.isCastingShadow !==
newObjectData.content.isCastingShadow
) {
this.setIsCastingShadow(newObjectData.content.isCastingShadow);
}
if (
oldObjectData.content.isReceivingShadow !==
newObjectData.content.isReceivingShadow
) {
this.setIsReceivingShadow(newObjectData.content.isReceivingShadow);
}
return true;
}
@@ -358,6 +376,16 @@ namespace gdjs {
return this._renderer.hasAnimationEnded();
}
setIsCastingShadow(value: boolean): void {
this._isCastingShadow = value;
this._renderer._updateShadow();
}
setIsReceivingShadow(value: boolean): void {
this._isReceivingShadow = value;
this._renderer._updateShadow();
}
setCrossfadeDuration(duration: number): void {
if (this._crossfadeDuration === duration) return;
this._crossfadeDuration = duration;

View File

@@ -286,6 +286,7 @@ namespace gdjs {
this.get3DRendererObject().remove(this._threeObject);
this.get3DRendererObject().add(threeObject);
this._threeObject = threeObject;
this._updateShadow();
// Start the current animation on the new 3D object.
this._animationMixer = new THREE.AnimationMixer(root);
@@ -323,6 +324,13 @@ namespace gdjs {
return this._originalModel.animations[animationIndex].name;
}
_updateShadow() {
this._threeObject.traverse((child) => {
child.castShadow = this._model3DRuntimeObject._isCastingShadow;
child.receiveShadow = this._model3DRuntimeObject._isReceivingShadow;
});
}
/**
* Return true if animation has ended.
* The animation had ended if:

View File

@@ -75,9 +75,9 @@ module.exports = {
.getOrCreate('align')
.setValue(objectContent.align)
.setType('choice')
.addExtraInfo('left')
.addExtraInfo('center')
.addExtraInfo('right')
.addChoice('left', _('Left'))
.addChoice('center', _('Center'))
.addChoice('right', _('Right'))
.setLabel(_('Base alignment'))
.setGroup(_('Appearance'));
@@ -88,9 +88,9 @@ module.exports = {
.getOrCreate('verticalTextAlignment')
.setValue(objectContent.verticalTextAlignment)
.setType('choice')
.addExtraInfo('top')
.addExtraInfo('center')
.addExtraInfo('bottom')
.addChoice('top', _('Top'))
.addChoice('center', _('Center'))
.addChoice('bottom', _('Bottom'))
.setLabel(_('Vertical alignment'))
.setGroup(_('Appearance'));

View File

@@ -61,9 +61,9 @@ module.exports = {
.getOrCreate('align')
.setValue(objectContent.align)
.setType('choice')
.addExtraInfo('left')
.addExtraInfo('center')
.addExtraInfo('right')
.addChoice('left', _('Left'))
.addChoice('center', _('Center'))
.addChoice('right', _('Right'))
.setLabel(_('Alignment'))
.setGroup(_('Appearance'));
@@ -74,9 +74,9 @@ module.exports = {
.getOrCreate('verticalTextAlignment')
.setValue(objectContent.verticalTextAlignment)
.setType('choice')
.addExtraInfo('top')
.addExtraInfo('center')
.addExtraInfo('bottom')
.addChoice('top', _('Top'))
.addChoice('center', _('Center'))
.addChoice('bottom', _('Bottom'))
.setLabel(_('Vertical alignment'))
.setGroup(_('Appearance'));

View File

@@ -194,9 +194,9 @@ ParticleEmitterObject::GetProperties() const {
: GetRendererType() == Line ? "Line"
: "Image")
.SetType("choice")
.AddExtraInfo("Circle")
.AddExtraInfo("Line")
.AddExtraInfo("Image")
.AddChoice("Circle", _("Circle"))
.AddChoice("Line", _("Line"))
.AddChoice("Image", _("Image"))
.SetLabel(_("Particle type"))
.SetHasImpactOnOtherProperties(true);

View File

@@ -818,7 +818,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('yesorno', _('Treat as bullet?'), '', false)
.addParameter('yesorno', _('Treat as bullet'), '', false)
.setDefaultValue('false')
.getCodeExtraInformation()
.setFunctionName('setBullet');
@@ -852,7 +852,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('yesorno', _('Fixed rotation?'), '', false)
.addParameter('yesorno', _('Fixed rotation'), '', false)
.setDefaultValue('false')
.getCodeExtraInformation()
.setFunctionName('setFixedRotation');
@@ -886,7 +886,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('yesorno', _('Can sleep?'), '', false)
.addParameter('yesorno', _('Can sleep'), '', false)
.setDefaultValue('true')
.getCodeExtraInformation()
.setFunctionName('setSleepingAllowed');
@@ -1296,7 +1296,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Layer (1 - 16)'))
.addParameter('yesorno', _('Enable?'), '', false)
.addParameter('yesorno', _('Enable'), '', false)
.setDefaultValue('true')
.getCodeExtraInformation()
.setFunctionName('enableLayer');
@@ -1332,7 +1332,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Mask (1 - 16)'))
.addParameter('yesorno', _('Enable?'), '', false)
.addParameter('yesorno', _('Enable'), '', false)
.setDefaultValue('true')
.getCodeExtraInformation()
.setFunctionName('enableMask');
@@ -2409,7 +2409,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Joint ID'))
.addParameter('yesorno', _('Enable?'))
.addParameter('yesorno', _('Enable'))
.getCodeExtraInformation()
.setFunctionName('enableRevoluteJointLimits');
@@ -2488,7 +2488,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Joint ID'))
.addParameter('yesorno', _('Enable?'))
.addParameter('yesorno', _('Enable'))
.getCodeExtraInformation()
.setFunctionName('enableRevoluteJointMotor');
@@ -2727,7 +2727,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Joint ID'))
.addParameter('yesorno', _('Enable?'))
.addParameter('yesorno', _('Enable'))
.getCodeExtraInformation()
.setFunctionName('enablePrismaticJointLimits');
@@ -2806,7 +2806,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Joint ID'))
.addParameter('yesorno', _('Enable?'))
.addParameter('yesorno', _('Enable'))
.getCodeExtraInformation()
.setFunctionName('enablePrismaticJointMotor');
@@ -3486,7 +3486,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Joint ID'))
.addParameter('yesorno', _('Enable?'))
.addParameter('yesorno', _('Enable'))
.getCodeExtraInformation()
.setFunctionName('enableWheelJointMotor');

View File

@@ -274,7 +274,7 @@ module.exports = {
.setLabel('Fixed Rotation')
.setDescription(
_(
"If enabled, the object won't rotate and will stay at the same angle. Useful for characters for example."
"If enabled, the object won't rotate and will stay at the same angle."
)
)
.setGroup(_('Movement'));
@@ -845,7 +845,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.addParameter('yesorno', _('Treat as bullet?'), '', false)
.addParameter('yesorno', _('Treat as bullet'), '', false)
.setDefaultValue('false')
.getCodeExtraInformation()
.setFunctionName('setBullet');
@@ -870,7 +870,7 @@ module.exports = {
'SetFixedRotation',
_('Fixed rotation'),
_(
"Enable or disable an object fixed rotation. If enabled the object won't be able to rotate."
"Enable or disable an object fixed rotation. If enabled the object won't be able to rotate. This action has no effect on characters."
),
_('Set _PARAM0_ fixed rotation: _PARAM2_'),
_('Dynamics'),
@@ -879,7 +879,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.addParameter('yesorno', _('Fixed rotation?'), '', false)
.addParameter('yesorno', _('Fixed rotation'), '', false)
.setDefaultValue('false')
.getCodeExtraInformation()
.setFunctionName('setFixedRotation');
@@ -1054,7 +1054,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.addParameter('expression', _('Layer (1 - 8)'))
.addParameter('yesorno', _('Enable?'), '', false)
.addParameter('yesorno', _('Enable'), '', false)
.setDefaultValue('true')
.getCodeExtraInformation()
.setFunctionName('enableLayer');
@@ -1090,7 +1090,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.addParameter('expression', _('Mask (1 - 8)'))
.addParameter('yesorno', _('Enable?'), '', false)
.addParameter('yesorno', _('Enable'), '', false)
.setDefaultValue('true')
.getCodeExtraInformation()
.setFunctionName('enableMask');
@@ -1270,7 +1270,7 @@ module.exports = {
.addParameter('expression', _('Application point on Z axis'))
.setParameterLongDescription(
_(
'Use `MassCenterX` and `MassCenterY` expressions to avoid any rotation.'
'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.'
)
)
.getCodeExtraInformation()
@@ -1544,6 +1544,19 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.getCodeExtraInformation()
.setFunctionName('getMassCenterY');
aut
.addExpression(
'MassCenterZ',
_('Mass center Z'),
_('Mass center Z'),
'',
'JsPlatform/Extensions/physics3d.svg'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
.getCodeExtraInformation()
.setFunctionName('getMassCenterZ');
}
// Collision
extension

View File

@@ -927,9 +927,7 @@ namespace gdjs {
const angularVelocityY = angularVelocity.GetY();
const angularVelocityZ = angularVelocity.GetZ();
let bodyID = this._body.GetID();
bodyInterface.RemoveBody(bodyID);
bodyInterface.DestroyBody(bodyID);
this.bodyUpdater.destroyBody();
this._contactsEndedThisFrame.length = 0;
this._contactsStartedThisFrame.length = 0;
this._currentContacts.length = 0;
@@ -938,7 +936,7 @@ namespace gdjs {
if (!this._body) {
return;
}
bodyID = this._body.GetID();
const bodyID = this._body.GetID();
bodyInterface.SetLinearVelocity(
bodyID,
this.getVec3(linearVelocityX, linearVelocityY, linearVelocityZ)

View File

@@ -733,7 +733,7 @@ namespace gdjs {
}
setWheelOffsetZ(wheelOffsetZ: float): void {
this._wheelOffsetY = wheelOffsetZ;
this._wheelOffsetZ = wheelOffsetZ;
this._updateWheels();
}
@@ -783,11 +783,11 @@ namespace gdjs {
}
hasFrontWheelDrive(): boolean {
return this._hasBackWheelDrive;
return this._hasFrontWheelDrive;
}
setFrontWheelDrive(hasFrontWheelDrive: boolean): void {
this._hasBackWheelDrive = hasFrontWheelDrive;
this._hasFrontWheelDrive = hasFrontWheelDrive;
this.invalidateShape();
}

View File

@@ -486,7 +486,16 @@ namespace gdjs {
this._state.beforeMovingX();
//Ensure the object is not stuck
if (this._separateFromPlatforms(this._potentialCollidingObjects, true)) {
const hasPopOutOfPlatform = this._separateFromPlatforms(
this._potentialCollidingObjects,
true
);
if (hasPopOutOfPlatform && !this._jumpKey) {
// TODO This is probably unnecessary because `_canJump` is already set
// to true when entering the `OnFloor` state.
// This is wrongly allowing double jumps when characters are flipped
// with an offset center.
//After being unstuck, the object must be able to jump again.
this._canJump = true;
}

View File

@@ -246,10 +246,10 @@ std::map<gd::String, gd::PropertyDescriptor> ShapePainterObject::GetProperties()
objectProperties["antialiasing"]
.SetValue(GetAntialiasing())
.SetType("choice")
.AddExtraInfo("none")
.AddExtraInfo("low")
.AddExtraInfo("medium")
.AddExtraInfo("high")
.AddChoice("none", _("None"))
.AddChoice("low", _("Low quality"))
.AddChoice("medium", _("Medium quality"))
.AddChoice("high", _("High quality"))
.SetGroup(_("Drawing"))
.SetLabel(_("Antialiasing"))
.SetDescription(_("Antialiasing mode"));

View File

@@ -195,11 +195,35 @@ namespace gdjs {
}
/**
* To be called when the game is disposed.
* Clear the Spine Atlases loaded in this manager.
* Clear the Spine atlases loaded in this manager.
*/
dispose(): void {
this._loadedSpineAtlases.clear();
this._loadingSpineAtlases.clear();
}
/**
* Unload the specified list of resources:
* this clears the Spine atlases loaded in this manager.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
if (loadedSpineAtlas) {
loadedSpineAtlas.dispose();
this._loadedSpineAtlases.delete(resourceData);
}
const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
if (loadingSpineAtlas) {
loadingSpineAtlas.then((atl) => atl.dispose());
this._loadingSpineAtlases.delete(resourceData);
}
});
}
}
}

View File

@@ -126,5 +126,22 @@ namespace gdjs {
dispose(): void {
this._loadedSpines.clear();
}
/**
* Unload the specified list of resources:
* this clears the Spine skeleton data loaded in this manager.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedSpine = this._loadedSpines.get(resourceData);
if (loadedSpine) {
this._loadedSpines.delete(resourceData);
}
});
}
}
}

View File

@@ -131,14 +131,14 @@ module.exports = {
.getOrCreate('inputType')
.setValue(objectContent.inputType || '')
.setType('choice')
.addExtraInfo('text')
.addExtraInfo('text area')
.addExtraInfo('email')
.addExtraInfo('password')
.addExtraInfo('number')
.addExtraInfo('telephone number')
.addExtraInfo('url')
.addExtraInfo('search')
.addChoice('text', _('Text'))
.addChoice('text area', _('Text area'))
.addChoice('email', _('Email'))
.addChoice('password', _('Password'))
.addChoice('number', _('Number'))
.addChoice('telephone number', _('Telephone number'))
.addChoice('url', _('URL'))
.addChoice('search', _('Search'))
.setLabel(_('Input type'))
.setDescription(
_(
@@ -250,9 +250,9 @@ module.exports = {
.getOrCreate('textAlign')
.setValue(objectContent.textAlign || 'left')
.setType('choice')
.addExtraInfo('left')
.addExtraInfo('center')
.addExtraInfo('right')
.addChoice('left', _('Left'))
.addChoice('center', _('Center'))
.addChoice('right', _('Right'))
.setLabel(_('Text alignment'))
.setGroup(_('Field appearance'));

View File

@@ -160,9 +160,9 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
objectProperties["textAlignment"]
.SetValue(textAlignment)
.SetType("choice")
.AddExtraInfo("left")
.AddExtraInfo("center")
.AddExtraInfo("right")
.AddChoice("left", _("Left"))
.AddChoice("center", _("Center"))
.AddChoice("right", _("Right"))
.SetLabel(_("Alignment"))
.SetDescription(_("Alignment of the text when multiple lines are displayed"))
.SetGroup(_("Font"))
@@ -171,9 +171,9 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
objectProperties["verticalTextAlignment"]
.SetValue(verticalTextAlignment)
.SetType("choice")
.AddExtraInfo("top")
.AddExtraInfo("center")
.AddExtraInfo("bottom")
.AddChoice("top", _("Top"))
.AddChoice("center", _("Center"))
.AddChoice("bottom", _("Bottom"))
.SetLabel(_("Vertical alignment"))
.SetGroup(_("Font"))
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);

View File

@@ -102,9 +102,9 @@ const defineTileMap = function (extension, _, gd) {
'displayMode',
new gd.PropertyDescriptor(objectContent.displayMode)
.setType('choice')
.addExtraInfo('visible')
.addExtraInfo('all')
.addExtraInfo('index')
.addChoice('visible', _('Visible layers'))
.addChoice('all', _('All layers'))
.addChoice('index', _('Only the layer with the specified index'))
.setLabel(_('Display mode'))
.setGroup(_('Appearance'))
);

View File

@@ -253,7 +253,12 @@ namespace gdjs {
// Let behaviors do something before the object is destroyed.
super.onDeletedFromScene();
// Destroy the children.
this._instanceContainer.onDestroyFromScene(this._runtimeScene);
this._instanceContainer.onDeletedFromScene(this._runtimeScene);
}
override onDestroyed(): void {
this._instanceContainer._destroy();
super.onDestroyed();
}
override update(parent: gdjs.RuntimeInstanceContainer): void {
@@ -276,6 +281,8 @@ namespace gdjs {
/**
* This method is called when the preview is being hot-reloaded.
*
* Custom objects implement this method with code generated from events.
*/
onHotReloading(parent: gdjs.RuntimeInstanceContainer) {}
@@ -284,6 +291,8 @@ namespace gdjs {
/**
* This method is called each tick after events are done.
*
* Custom objects implement this method with code generated from events.
* @param parent The instanceContainer owning the object
*/
doStepPostEvents(parent: gdjs.RuntimeInstanceContainer) {}
@@ -291,6 +300,8 @@ namespace gdjs {
/**
* This method is called when the object is being removed from its parent
* container and is about to be destroyed/reused later.
*
* Custom objects implement this method with code generated from events.
*/
onDestroy(parent: gdjs.RuntimeInstanceContainer) {}

View File

@@ -71,7 +71,7 @@ namespace gdjs {
eventsBasedObjectVariantData: EventsBasedObjectVariantData
) {
if (this._isLoaded) {
this.onDestroyFromScene(this._parent);
this.onDeletedFromScene(this._parent);
}
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
@@ -186,26 +186,25 @@ namespace gdjs {
*
* @param instanceContainer The container owning the object.
*/
onDestroyFromScene(instanceContainer: gdjs.RuntimeInstanceContainer): void {
onDeletedFromScene(instanceContainer: gdjs.RuntimeInstanceContainer): void {
if (!this._isLoaded) {
return;
}
// Notify the objects they are being destroyed
const allInstancesList = this.getAdhocListOfAllInstances();
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const object = allInstancesList[i];
object.onDeletedFromScene();
// The object can free all its resource directly...
object.onDestroyed();
}
// ...as its container cache `_instancesRemoved` is also destroy.
this._destroy();
this._isLoaded = false;
}
_destroy() {
override _destroy() {
const allInstancesList = this.getAdhocListOfAllInstances();
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const object = allInstancesList[i];
object.onDestroyed();
}
// It should not be necessary to reset these variables, but this help
// ensuring that all memory related to the container is released immediately.
super._destroy();

View File

@@ -162,5 +162,29 @@ namespace gdjs {
this._invalidModel.scene.clear();
}
}
/**
* Unload the specified list of resources:
* this clears the models, resources loaded and destroy 3D models loaders in this manager.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
if (loadedThreeModel) {
loadedThreeModel.scene.clear();
this._loadedThreeModels.delete(resourceData);
}
const downloadedArrayBuffer =
this._downloadedArrayBuffers.get(resourceData);
if (downloadedArrayBuffer) {
this._downloadedArrayBuffers.delete(resourceData);
}
});
}
}
}

View File

@@ -5,6 +5,7 @@
*/
namespace gdjs {
const logger = new gdjs.Logger('ResourceLoader');
const debugLogger = new gdjs.Logger('ResourceLoader - debug').enable(false);
const addSearchParameterToUrl = (
url: string,
@@ -96,17 +97,15 @@ namespace gdjs {
*/
private _globalResources: Array<string>;
/**
* Resources by scene names.
* Resources and the loading state of each scene, indexed by scene name.
*/
private _sceneResources: Map<string, Array<string>>;
/**
* Keep track of which scene whose resources has already be pre-loaded.
*/
private _sceneNamesToLoad: Set<string>;
/**
* Keep track of which scene whose resources has already be loaded.
*/
private _sceneNamesToMakeReady: Set<string>;
private _sceneLoadingStates: Map<
string,
{
resourceNames: Array<string>;
status: 'not-loaded' | 'loaded' | 'ready';
}
> = new Map();
/**
* A queue of scenes whose resources are still to be pre-loaded.
*/
@@ -127,11 +126,12 @@ namespace gdjs {
private _spineManager: SpineManager | null = null;
/**
* Only used by events.
* The name of the scene for which resources are currently being loaded.
*/
private currentLoadingSceneName: string = '';
/**
* Only used by events.
* The progress, between 0 and 1, of the loading of the resource, for the
* scene that is being loaded (see `currentLoadingSceneName`).
*/
private currentSceneLoadingProgress: float = 0;
/**
@@ -144,8 +144,8 @@ namespace gdjs {
/**
* @param runtimeGame The game.
* @param resourceDataArray The resources data of the game.
* @param globalResources The resources needed for any layer.
* @param layoutDataArray The resources used by each layer.
* @param globalResources The resources needed for any scene.
* @param layoutDataArray The resources used by each scene.
*/
constructor(
runtimeGame: RuntimeGame,
@@ -158,9 +158,6 @@ namespace gdjs {
this._globalResources = globalResources;
// These 3 attributes are filled by `setResources`.
this._sceneResources = new Map<string, Array<string>>();
this._sceneNamesToLoad = new Set<string>();
this._sceneNamesToMakeReady = new Set<string>();
this.setResources(resourceDataArray, globalResources, layoutDataArray);
this._imageManager = new gdjs.ImageManager(this);
@@ -224,23 +221,31 @@ namespace gdjs {
): void {
this._globalResources = globalResources;
this._sceneResources.clear();
this._sceneNamesToLoad.clear();
this._sceneNamesToMakeReady.clear();
this._sceneLoadingStates.clear();
for (const layoutData of layoutDataArray) {
this._sceneResources.set(
layoutData.name,
layoutData.usedResources.map((resource) => resource.name)
);
this._sceneNamesToLoad.add(layoutData.name);
this._sceneNamesToMakeReady.add(layoutData.name);
this._sceneLoadingStates.set(layoutData.name, {
resourceNames: layoutData.usedResources.map(
(resource) => resource.name
),
status: 'not-loaded',
});
}
// TODO Clearing the queue doesn't abort the running task, but it should
// not matter as resource loading is really fast in preview mode.
this._sceneToLoadQueue.length = 0;
for (let index = layoutDataArray.length - 1; index >= 0; index--) {
const layoutData = layoutDataArray[index];
this._sceneToLoadQueue.push(new SceneLoadingTask(layoutData.name));
const resourcesPreloading = layoutData.resourcesPreloading || 'inherit';
const resolvedResourcesPreloading =
resourcesPreloading === 'inherit'
? this._runtimeGame.getSceneResourcesPreloading()
: resourcesPreloading;
if (resolvedResourcesPreloading === 'at-startup') {
this._sceneToLoadQueue.push(new SceneLoadingTask(layoutData.name));
}
}
this._resources.clear();
@@ -271,8 +276,10 @@ namespace gdjs {
onProgress(loadedCount, this._resources.size);
}
);
this._sceneNamesToLoad.clear();
this._sceneNamesToMakeReady.clear();
for (const sceneLoadingState of this._sceneLoadingStates.values()) {
sceneLoadingState.status = 'ready';
}
}
/**
@@ -282,17 +289,21 @@ namespace gdjs {
firstSceneName: string,
onProgress: (count: number, total: number) => void
): Promise<void> {
const sceneResources = this._sceneResources.get(firstSceneName);
if (!sceneResources) {
const firstSceneState = this._sceneLoadingStates.get(firstSceneName);
if (!firstSceneState) {
logger.warn(
'Can\'t load resource for unknown scene: "' + firstSceneName + '".'
);
return;
}
let loadedCount = 0;
const resources = [...this._globalResources, ...sceneResources.values()];
const resourceNames = [
...this._globalResources,
...firstSceneState.resourceNames,
];
await processAndRetryIfNeededWithPromisePool(
resources,
resourceNames,
maxForegroundConcurrency,
maxAttempt,
async (resourceName) => {
@@ -304,11 +315,11 @@ namespace gdjs {
await this._loadResource(resource);
await this._processResource(resource);
loadedCount++;
onProgress(loadedCount, resources.length);
onProgress(loadedCount, resourceNames.length);
}
);
this._setSceneAssetsLoaded(firstSceneName);
this._setSceneAssetsReady(firstSceneName);
firstSceneState.status = 'ready';
}
/**
@@ -318,17 +329,32 @@ namespace gdjs {
* scenes.
*/
async loadAllSceneInBackground(): Promise<void> {
if (this.currentLoadingSceneName) {
return;
}
debugLogger.log('Loading all scene resources, in background.');
while (this._sceneToLoadQueue.length > 0) {
debugLogger.log(
`Still resources of ${this._sceneToLoadQueue.length} scene(s) to load: ${this._sceneToLoadQueue.map((task) => task.sceneName).join(', ')}`
);
const task = this._sceneToLoadQueue[this._sceneToLoadQueue.length - 1];
if (task === undefined) {
continue;
}
this.currentLoadingSceneName = task.sceneName;
if (!this.areSceneAssetsLoaded(task.sceneName)) {
debugLogger.log(
`Loading (but not processing) resources for scene ${task.sceneName}.`
);
await this._doLoadSceneResources(
task.sceneName,
async (count, total) => task.onProgress(count, total)
);
debugLogger.log(
`Done loading (but not processing) resources for scene ${task.sceneName}.`
);
// A scene may have been moved last while awaiting resources to be
// downloaded (see _prioritizeScene).
this._sceneToLoadQueue.splice(
@@ -340,6 +366,7 @@ namespace gdjs {
this._sceneToLoadQueue.pop();
}
}
debugLogger.log(`Scene resources loading finished.`);
this.currentLoadingSceneName = '';
}
@@ -347,16 +374,17 @@ namespace gdjs {
sceneName: string,
onProgress?: (count: number, total: number) => Promise<void>
): Promise<void> {
const sceneResources = this._sceneResources.get(sceneName);
if (!sceneResources) {
const sceneState = this._sceneLoadingStates.get(sceneName);
if (!sceneState) {
logger.warn(
'Can\'t load resource for unknown scene: "' + sceneName + '".'
);
return;
}
let loadedCount = 0;
await processAndRetryIfNeededWithPromisePool(
[...sceneResources.values()],
sceneState.resourceNames,
this._isLoadingInForeground
? maxForegroundConcurrency
: maxBackgroundConcurrency,
@@ -369,11 +397,13 @@ namespace gdjs {
}
await this._loadResource(resource);
loadedCount++;
this.currentSceneLoadingProgress = loadedCount / this._resources.size;
onProgress && (await onProgress(loadedCount, this._resources.size));
this.currentSceneLoadingProgress =
loadedCount / sceneState.resourceNames.length;
onProgress &&
(await onProgress(loadedCount, sceneState.resourceNames.length));
}
);
this._setSceneAssetsLoaded(sceneName);
sceneState.status = 'loaded';
}
private async _loadResource(resource: ResourceData): Promise<void> {
@@ -405,8 +435,8 @@ namespace gdjs {
}
await this.loadSceneResources(sceneName, onProgress);
const sceneResources = this._sceneResources.get(sceneName);
if (!sceneResources) {
const sceneState = this._sceneLoadingStates.get(sceneName);
if (!sceneState) {
logger.warn(
'Can\'t load resource for unknown scene: "' + sceneName + '".'
);
@@ -414,7 +444,7 @@ namespace gdjs {
}
let parsedCount = 0;
for (const resourceName of sceneResources) {
for (const resourceName of sceneState.resourceNames) {
const resource = this._resources.get(resourceName);
if (!resource) {
logger.warn('Unable to find resource "' + resourceName + '".');
@@ -422,9 +452,10 @@ namespace gdjs {
}
await this._processResource(resource);
parsedCount++;
onProgress && (await onProgress(parsedCount, sceneResources.length));
onProgress &&
(await onProgress(parsedCount, sceneState.resourceNames.length));
}
this._setSceneAssetsReady(sceneName);
sceneState.status = 'ready';
}
/**
@@ -438,15 +469,25 @@ namespace gdjs {
sceneName: string,
onProgress?: (count: number, total: number) => void
): Promise<void> {
debugLogger.log(
`Prioritization of loading of resources for scene ${sceneName} was requested.`
);
this._isLoadingInForeground = true;
const task = this._prioritizeScene(sceneName);
return new Promise<void>((resolve, reject) => {
if (!task) {
this._isLoadingInForeground = false;
debugLogger.log(
`Loading of resources for scene ${sceneName} was immediately resolved.`
);
resolve();
return;
}
task.registerCallback(() => {
debugLogger.log(
`Loading of resources for scene ${sceneName} just finished.`
);
this._isLoadingInForeground = false;
resolve();
}, onProgress);
@@ -463,6 +504,51 @@ namespace gdjs {
}
}
/**
* To be called when a scene is unloaded.
*/
unloadSceneResources({
unloadedSceneName,
newSceneName,
}: {
unloadedSceneName: string;
newSceneName: string | null;
}): void {
if (!unloadedSceneName) return;
debugLogger.log(
`Unloading of resources for scene ${unloadedSceneName} was requested.`
);
const sceneUniqueResourcesByKindMap =
this._getResourcesByKindOnlyUsedInUnloadedScene({
unloadedSceneName,
newSceneName,
});
for (const [kindResourceManager, resourceManager] of this
._resourceManagersMap) {
const resources =
sceneUniqueResourcesByKindMap.get(kindResourceManager);
if (resources) {
debugLogger.log(
`Unloading of resources of kind ${kindResourceManager} for scene ${unloadedSceneName}: `,
resources.map((resource) => resource.name).join(', ')
);
resourceManager.unloadResourcesList(resources);
}
}
debugLogger.log(
`Unloading of resources for scene ${unloadedSceneName} finished.`
);
const sceneState = this._sceneLoadingStates.get(unloadedSceneName);
if (sceneState) {
sceneState.status = 'not-loaded';
}
// TODO: mark the scene as unloaded so it's not automatically loaded again eagerly.
}
/**
* Put a given scene at the end of the queue.
*
@@ -470,16 +556,41 @@ namespace gdjs {
* this scene will be the next to be loaded.
*/
private _prioritizeScene(sceneName: string): SceneLoadingTask | null {
const sceneState = this._sceneLoadingStates.get(sceneName);
if (!sceneState) return null;
if (sceneState.status === 'loaded' || sceneState.status === 'ready') {
debugLogger.log(
`Scene ${sceneName} is already loaded. Skipping prioritization.`
);
// The scene is already loaded, nothing to do.
return null;
}
// The scene is not loaded: either prioritize it or add it to the loading queue.
const taskIndex = this._sceneToLoadQueue.findIndex(
(task) => task.sceneName === sceneName
);
if (taskIndex < 0) {
// The scene is already loaded.
return null;
let task: SceneLoadingTask;
if (taskIndex !== -1) {
// There is already a task for this scene in the queue.
// Move it so that it's loaded first.
task = this._sceneToLoadQueue[taskIndex];
this._sceneToLoadQueue.splice(taskIndex, 1);
this._sceneToLoadQueue.push(task);
} else {
// There is no task for this scene in the queue.
// It might be because the scene was unloaded or never loaded.
// In this case, we need to add a new task to the queue.
task = new SceneLoadingTask(sceneName);
this._sceneToLoadQueue.push(task);
}
const task = this._sceneToLoadQueue[taskIndex];
this._sceneToLoadQueue.splice(taskIndex, 1);
this._sceneToLoadQueue.push(task);
// Re-start the loading process in the background. While at the beginning of the game
// it's not needed because already launched, a scene might be unloaded. This means
// that we then need to relaunch the loading process.
this.loadAllSceneInBackground();
return task;
}
@@ -511,7 +622,10 @@ namespace gdjs {
* (but maybe not parsed).
*/
areSceneAssetsLoaded(sceneName: string): boolean {
return !this._sceneNamesToLoad.has(sceneName);
const sceneState = this._sceneLoadingStates.get(sceneName);
if (!sceneState) return false;
return sceneState.status === 'loaded' || sceneState.status === 'ready';
}
/**
@@ -519,15 +633,10 @@ namespace gdjs {
* parsed.
*/
areSceneAssetsReady(sceneName: string): boolean {
return !this._sceneNamesToMakeReady.has(sceneName);
}
const sceneState = this._sceneLoadingStates.get(sceneName);
if (!sceneState) return false;
private _setSceneAssetsLoaded(sceneName: string): void {
this._sceneNamesToLoad.delete(sceneName);
}
private _setSceneAssetsReady(sceneName: string): void {
this._sceneNamesToMakeReady.delete(sceneName);
return sceneState.status === 'ready';
}
getResource(resourceName: string): ResourceData | null {
@@ -636,6 +745,70 @@ namespace gdjs {
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
return this._spineAtlasManager;
}
injectMockResourceManagerForTesting(
resourceKind: ResourceKind,
resourceManager: ResourceManager
) {
this._resourceManagersMap.set(resourceKind, resourceManager);
}
/**
* Get the map of resources that are only used in the scene that is being unloaded,
* and that are not used in any other loaded scene (or the scene that is coming next).
*/
private _getResourcesByKindOnlyUsedInUnloadedScene({
unloadedSceneName,
newSceneName,
}: {
unloadedSceneName: string;
newSceneName: string | null;
}): Map<ResourceKind, ResourceData[]> {
const unloadedSceneState =
this._sceneLoadingStates.get(unloadedSceneName);
if (!unloadedSceneState) {
return new Map<ResourceKind, ResourceData[]>();
}
// Construct the set of all resources to unload. These are the resources
// used in the scene that is being unloaded minus all the resources used
// by the other scenes that are loaded (and the possible scene that is coming next).
const resourceNamesToUnload = new Set<string>(
unloadedSceneState.resourceNames
);
for (const [
sceneName,
sceneState,
] of this._sceneLoadingStates.entries()) {
if (sceneName === unloadedSceneName) continue;
if (
sceneName === newSceneName ||
sceneState.status === 'loaded' ||
sceneState.status === 'ready'
) {
sceneState.resourceNames.forEach((resourceName) => {
resourceNamesToUnload.delete(resourceName);
});
}
}
const result = new Map<ResourceKind, ResourceData[]>();
resourceNamesToUnload.forEach((resourceName) => {
const resourceData = this._resources.get(resourceName);
if (!resourceData) return;
const kind = resourceData.kind;
const resources = result.get(kind);
if (resources) {
resources.push(resourceData);
} else {
result.set(kind, [resourceData]);
}
});
return result;
}
}
type PromiseError<T> = { item: T; error: Error };

View File

@@ -35,5 +35,15 @@ namespace gdjs {
* Using the manager after calling this method is undefined behavior.
*/
dispose(): void;
/**
* Should clear all specified resources data and anything stored by this manager
* for these resources.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources that need to be clear
*/
unloadResourcesList(resourcesList: ResourceData[]): void;
}
}

View File

@@ -116,6 +116,9 @@ namespace gdjs {
}
}
getRuntimeLayer(): gdjs.RuntimeLayer {
return this;
}
getRenderer(): gdjs.LayerRenderer {
return this._renderer;
}

View File

@@ -738,7 +738,6 @@ namespace gdjs {
// scene (see `_hotReloadRuntimeInstanceContainer` call from
// `_hotReloadRuntimeSceneInstances`).
objects: mergedChildObjectDataList,
childrenContent: mergedChildObjectDataList,
};
return mergedObjectConfiguration;
});

View File

@@ -362,8 +362,7 @@ namespace gdjs {
if (objectsLists.items.hasOwnProperty(name)) {
const allObjects = objectsContext.getObjects(name);
const objectsList = objectsLists.items[name];
objectsList.length = 0;
objectsList.push.apply(objectsList, allObjects);
gdjs.copyArray(allObjects, objectsList);
}
}
return true;

View File

@@ -205,6 +205,28 @@ namespace gdjs {
this._loadedFontFamily.clear();
this._loadedFontFamilySet.clear();
}
/**
* Unload the specified list of resources:
* this clears the caches of loaded font families.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const resource = this._loadedFontFamily.get(resourceData);
if (resource) {
this._loadedFontFamily.delete(resourceData);
}
const fontName = this._getFontFamilyFromFilename(resourceData);
if (fontName) {
this._loadedFontFamilySet.delete(fontName);
}
});
}
}
//Register the class to let the engine use it.

View File

@@ -939,6 +939,28 @@ namespace gdjs {
dispose(): void {
this.unloadAll();
}
/**
* Unload the specified list of resources:
* this unloads all audio from the specified resources from memory.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const musicRes = this._loadedMusics.get(resourceData);
if (musicRes) {
this.unloadAudio(resourceData.name, true);
}
const soundRes = this._loadedSounds.get(resourceData);
if (soundRes) {
this.unloadAudio(resourceData.name, false);
}
});
}
}
// Register the class to let the engine use it.

View File

@@ -208,5 +208,26 @@ namespace gdjs {
this._loadedJsons.clear();
this._callbacks.clear();
}
/**
* Unload the specified list of resources:
* this clears the JSONs loaded in this manager.
*
* Usually called when scene resoures are unloaded.
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedJson = this._loadedJsons.get(resourceData);
if (loadedJson) {
this._loadedJsons.delete(resourceData);
}
const callback = this._callbacks.get(resourceData);
if (callback) {
this._callbacks.delete(resourceData);
}
});
}
}
}

View File

@@ -66,6 +66,7 @@ namespace gdjs {
*/
export class Logger {
private readonly group: string;
private enabled: boolean = true;
/**
* Create a new logger with the given group name.
@@ -76,21 +77,30 @@ namespace gdjs {
}
log(...messages: any[]): void {
if (!this.enabled) return;
loggerOutput.log(this.group, objectsToString(messages), 'info');
}
info(...messages: any[]): void {
if (!this.enabled) return;
loggerOutput.log(this.group, objectsToString(messages), 'info');
}
warn(...messages: any[]): void {
if (!this.enabled) return;
loggerOutput.log(this.group, objectsToString(messages), 'warning');
}
error(...messages: any[]): void {
if (!this.enabled) return;
loggerOutput.log(this.group, objectsToString(messages), 'error');
}
enable(enabled: boolean): gdjs.Logger {
this.enabled = enabled;
return this;
}
/**
* Give access to the console output used by default by the logger.
* This can be useful to restore the default log method if you overrode it

View File

@@ -307,6 +307,34 @@ namespace gdjs {
this._pixiBitmapFontsToUninstall.length = 0;
this._loadedFontsData.clear();
}
/**
* Unload the specified list of resources:
* this uninstalls fonts from memory and clear cache of loaded fonts.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedFont = this._loadedFontsData.get(resourceData);
if (loadedFont) {
this._loadedFontsData.delete(resourceData);
}
for (const bitmapFontInstallKey in this._pixiBitmapFontsInUse) {
if (bitmapFontInstallKey.endsWith(resourceData.file))
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
}
for (const bitmapFontInstallKey of this._pixiBitmapFontsToUninstall) {
if (bitmapFontInstallKey.endsWith(resourceData.file))
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
}
});
}
}
// Register the class to let the engine use it.

View File

@@ -17,6 +17,7 @@ namespace gdjs {
getName: () => string;
getRendererObject: () => RendererObjectInterface | null | undefined;
get3DRendererObject: () => THREE.Object3D | null | undefined;
getRuntimeLayer?: () => gdjs.RuntimeLayer;
}
/**

View File

@@ -519,6 +519,37 @@ namespace gdjs {
}
this._scaledTextures.clear();
}
/**
* Unload the specified list of resources:
* this clears the cache of loaded textures associated to these resources.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const resourceName = resourceData.name;
const resource = this._loadedTextures.get(resourceData);
if (resource) {
resource.destroy(true);
this._loadedTextures.delete(resourceData);
}
const threeTexture = this._loadedThreeTextures.get(resourceName);
if (threeTexture) {
threeTexture.dispose();
this._loadedThreeTextures.remove(resourceName);
}
const threeMaterials = this._loadedThreeMaterials.get(resourceName);
if (threeMaterials) {
threeMaterials.dispose();
this._loadedThreeMaterials.remove(resourceName);
}
});
}
}
//Register the class to let the engine use it.

View File

@@ -97,6 +97,8 @@ namespace gdjs {
!gdjs.evtTools.common.isMobile()),
preserveDrawingBuffer: true, // Keep to true to allow screenshots.
});
this._threeRenderer.shadowMap.enabled = true;
this._threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
this._threeRenderer.useLegacyLights = true;
this._threeRenderer.autoClear = false;
this._threeRenderer.setSize(

View File

@@ -184,6 +184,9 @@ namespace gdjs {
*/
_embeddedResourcesMappings: Map<string, Record<string, string>>;
_sceneResourcesPreloading: 'at-startup' | 'never';
_sceneResourcesUnloading: 'at-scene-exit' | 'never';
/**
* Optional client to connect to a debugger server.
*/
@@ -223,6 +226,11 @@ namespace gdjs {
this._data = data;
this._updateSceneAndExtensionsData();
this._sceneResourcesPreloading =
this._data.properties.sceneResourcesPreloading || 'at-startup';
this._sceneResourcesUnloading =
this._data.properties.sceneResourcesUnloading || 'never';
this._resourcesLoader = new gdjs.ResourceLoader(
this,
data.resources.resources,
@@ -245,6 +253,7 @@ namespace gdjs {
this._antialiasingMode = this._data.properties.antialiasingMode;
this._isAntialisingEnabledOnMobile =
this._data.properties.antialisingEnabledOnMobile;
this._renderer = new gdjs.RuntimeGameRenderer(
this,
this._options.forceFullscreen || false
@@ -363,6 +372,14 @@ namespace gdjs {
return this._variablesByExtensionName.get(extensionName) || null;
}
/**
* Get the gdjs.ResourceLoader of the RuntimeGame.
* @return The resource loader.
*/
getResourceLoader(): gdjs.ResourceLoader {
return this._resourcesLoader;
}
/**
* Get the gdjs.SoundManager of the RuntimeGame.
* @return The sound manager.
@@ -764,6 +781,22 @@ namespace gdjs {
return this._resourcesLoader.areSceneAssetsReady(sceneName);
}
/**
* Returns the scene resources preloading mode.
* It can be overriden by each scene.
*/
getSceneResourcesPreloading(): 'at-startup' | 'never' {
return this._sceneResourcesPreloading;
}
/**
* Returns the scene resources unloading mode.
* It can be overriden by each scene.
*/
getSceneResourcesUnloading(): 'at-scene-exit' | 'never' {
return this._sceneResourcesUnloading;
}
/**
* Load all assets needed to display the 1st scene, displaying progress in
* renderer.

View File

@@ -292,7 +292,7 @@ namespace gdjs {
/**
* Called to reset the object to its default state. This is used for objects that are
* "recycled": they are dismissed (at which point `onDestroyFromScene` is called) but still
* "recycled": they are dismissed (at which point `onDeletedFromScene` is called) but still
* stored in a cache to be reused next time an object must be created. At this point,
* `reinitialize` will be called. The object must then work as if it was a newly constructed
* object.
@@ -596,7 +596,7 @@ namespace gdjs {
/**
* Remove an object from a scene.
*
* Do not change/redefine this method. Instead, redefine the onDestroyFromScene method.
* Do not change/redefine this method. Instead, redefine the onDeletedFromScene method.
*/
deleteFromScene(): void {
if (this._livingOnScene) {
@@ -614,9 +614,12 @@ namespace gdjs {
}
/**
* Called when the object is destroyed (because it is removed from a scene or the scene
* is being unloaded). If you redefine this function, **make sure to call the original method**
* (`RuntimeObject.prototype.onDestroyFromScene.call(this, runtimeScene);`).
* Called when the object is deleted (because it is removed from a scene or
* the scene is being unloaded). The object is not actually destroyed and
* can still be used by events.
*
* If you redefine this function, **make sure to call the original method**
* (`super.onDeletedFromScene();`).
*/
onDeletedFromScene(): void {
const theLayer = this._runtimeScene.getLayer(this.layer);
@@ -635,6 +638,10 @@ namespace gdjs {
this.clearEffects();
}
/**
* Called on deleted objects after all events has been executed for the
* current frame and the object can be safely destroyed.
*/
onDestroyed(): void {}
/**

View File

@@ -23,6 +23,7 @@ namespace gdjs {
_timeManager: TimeManager;
_gameStopRequested: boolean = false;
_requestedScene: string = '';
_resourcesUnloading: 'at-scene-exit' | 'never' | 'inherit' = 'inherit';
private _asyncTasksManager = new gdjs.AsyncTasksManager();
/** True if loadFromScene was called and the scene is being played. */
@@ -141,6 +142,7 @@ namespace gdjs {
this._runtimeGame.getRenderer().setWindowTitle(sceneData.title);
}
this._name = sceneData.name;
this._resourcesUnloading = sceneData.resourcesUnloading || 'inherit';
this.setBackgroundColor(sceneData.r, sceneData.v, sceneData.b);
//Load layers
@@ -306,7 +308,7 @@ namespace gdjs {
this.onGameResolutionResized();
}
_destroy() {
override _destroy() {
// It should not be necessary to reset these variables, but this help
// ensuring that all memory related to the RuntimeScene is released immediately.
super._destroy();
@@ -578,6 +580,13 @@ namespace gdjs {
return this._name;
}
/**
* Get the strategy to unload resources of this scene.
*/
getResourcesUnloading(): 'at-scene-exit' | 'never' | 'inherit' {
return this._resourcesUnloading;
}
/**
* Create an identifier for a new object of the scene.
*/

View File

@@ -63,10 +63,14 @@ namespace gdjs {
this.pop();
} else if (request === gdjs.SceneChangeRequest.PUSH_SCENE) {
this.push(currentScene.getRequestedScene());
} else if (request === gdjs.SceneChangeRequest.REPLACE_SCENE) {
this.replace(currentScene.getRequestedScene());
} else if (request === gdjs.SceneChangeRequest.CLEAR_SCENES) {
this.replace(currentScene.getRequestedScene(), true);
} else if (
request === gdjs.SceneChangeRequest.REPLACE_SCENE ||
request === gdjs.SceneChangeRequest.CLEAR_SCENES
) {
this.replace(
currentScene.getRequestedScene(),
request === gdjs.SceneChangeRequest.CLEAR_SCENES
);
} else {
logger.error('Unrecognized change in scene stack: ' + request);
}
@@ -101,7 +105,10 @@ namespace gdjs {
if (!scene) {
return;
}
scene.unloadScene();
this._unloadSceneAndPossiblyResources({
scene,
newSceneName: null,
});
}
// Tell the new current scene it's being resumed
@@ -140,6 +147,7 @@ namespace gdjs {
this._loadNewScene(newSceneName);
this._isNextLayoutLoading = false;
});
return null;
}
@@ -186,7 +194,7 @@ namespace gdjs {
while (this._stack.length !== 0) {
let scene = this._stack.pop();
if (scene) {
scene.unloadScene();
this._unloadSceneAndPossiblyResources({ scene, newSceneName });
}
}
} else {
@@ -194,7 +202,7 @@ namespace gdjs {
if (this._stack.length !== 0) {
let scene = this._stack.pop();
if (scene) {
scene.unloadScene();
this._unloadSceneAndPossiblyResources({ scene, newSceneName });
}
}
}
@@ -372,14 +380,52 @@ namespace gdjs {
* Unload all the scenes and clear the stack.
*/
dispose(): void {
for (const item of this._stack) {
item.unloadScene();
while (this._stack.length > 0) {
const scene = this._stack.pop();
if (scene) {
this._unloadSceneAndPossiblyResources({
scene,
newSceneName: null,
});
}
}
this._stack.length = 0;
this._wasDisposed = true;
}
private _unloadSceneAndPossiblyResources({
scene,
newSceneName,
}: {
scene: gdjs.RuntimeScene;
newSceneName: string | null;
}): void {
const unloadedSceneName = scene.getName();
const resourcesUnloading = scene.getResourcesUnloading();
const resolvedResourcesUnloading =
resourcesUnloading === 'inherit'
? this._runtimeGame.getSceneResourcesUnloading()
: resourcesUnloading;
const shouldUnloadResources =
resolvedResourcesUnloading === 'at-scene-exit' &&
// Unload resources only if it's the last scene with this name in the stack.
newSceneName !== scene.getName() &&
this._stack.every((scene) => scene.getName() !== unloadedSceneName);
scene.unloadScene();
// After this point, `scene` is no longer valid and should not be used anymore.
// It was "disposed".
if (shouldUnloadResources) {
this._runtimeGame.getResourceLoader().unloadSceneResources({
unloadedSceneName,
newSceneName,
});
}
}
private _throwIfDisposed(): void {
if (this._wasDisposed) {
throw 'The scene stack has been disposed and should not be used anymore.';

View File

@@ -169,6 +169,8 @@ declare interface LayoutData extends InstanceContainerData {
title: string;
behaviorsSharedData: BehaviorSharedData[];
usedResources: ResourceReference[];
resourcesPreloading?: 'at-startup' | 'never' | 'inherit';
resourcesUnloading?: 'at-scene-exit' | 'never' | 'inherit';
}
declare interface LayoutNetworkSyncData {
@@ -370,6 +372,8 @@ declare interface ProjectPropertiesData {
extensionProperties: Array<ExtensionProperty>;
useDeprecatedZeroAsDefaultZOrder?: boolean;
projectUuid?: string;
sceneResourcesPreloading?: 'at-startup' | 'never';
sceneResourcesUnloading?: 'at-scene-exit' | 'never';
}
declare interface ExtensionProperty {
@@ -437,4 +441,5 @@ declare type ResourceKind =
| 'bitmapFont'
| 'model3D'
| 'atlas'
| 'spine';
| 'spine'
| 'fake-resource-kind-for-testing-only';

View File

@@ -225,7 +225,7 @@ const identifyClassNames = (code) => {
.replace(/( hide\w*\(.*\))(: any)? {/g, '$1: void {')
.replace(/( reset\w*\(.*\))(: any)? {/g, '$1: void {')
.replace(/( deleteFromScene\w*\(.*\))(: any)? {/g, '$1: void {')
.replace(/( onDestroyFromScene\w*\(.*\))(: any)? {/g, '$1: void {')
.replace(/( onDeletedFromScene\w*\(.*\))(: any)? {/g, '$1: void {')
.replace(/( enable\w*\(.*\))(: any)? {/g, '$1: void {')
// Members:
.replace(/(\w*): any = (\d)/g, '$1: number = $2')

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,47 @@
// @ts-check
/*
* GDevelop JS Platform
* Copyright 2013-2023 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
// implements gdjs.ResourceManager
/**
* @implements {gdjs.ResourceManager}
*/
gdjs.MockedResourceManager = class MockedResourceManager {
loadResourcePromises = new Map();
loadResourceCallbacks = new Map();
disposedResources = new Set();
loadedResources = new Set();
waitingForProcessing = new Set();
readyResources = new Set();
loadResource(resourceName) {
const that = this;
return new Promise((resolve, reject) => {
that.loadResourceCallbacks.set(resourceName, resolve);
if (
this.loadedResources.has(resourceName) ||
this.waitingForProcessing.has(resourceName)
) {
return Promise.resolve();
}
const existingPromise = this.loadResourcePromises.get(resourceName);
if (existingPromise) {
return existingPromise;
}
const promise = new Promise((resolve) => {
this.loadResourceCallbacks.set(resourceName, resolve);
});
this.loadResourcePromises.set(resourceName, promise);
return promise;
}
async processResource(resourceName) {}
async processResource(resourceName) {
// Mark resource as fully processed
this.readyResources.add(resourceName);
}
/**
* @param {string} resourceName
* @param {string} resourceName
* @returns {boolean}
*/
isResourceDownloadPending(resourceName) {
@@ -26,15 +49,64 @@ gdjs.MockedResourceManager = class MockedResourceManager {
}
/**
* @param {string} resourceName
* @param {string} resourceName
*/
markPendingResourcesAsLoaded(resourceName) {
const loadResourceCallback = this.loadResourceCallbacks.get(resourceName);
loadResourceCallback();
this.loadResourceCallbacks.delete(resourceName);
if (loadResourceCallback) {
this.loadedResources.add(resourceName);
loadResourceCallback();
this.loadResourceCallbacks.delete(resourceName);
this.loadResourcePromises.delete(resourceName);
} else {
throw new Error(
`Resource ${resourceName} was not being loaded, so cannot be marked as loaded.`
);
}
}
/**
* Check if a resource is loaded (but maybe not yet processed)
*/
isResourceLoaded(resourceName) {
return this.loadedResources.has(resourceName);
}
/**
* Check if a resource has been disposed
*/
isResourceDisposed(resourceName) {
return this.disposedResources.has(resourceName);
}
/**
* Dispose all resources
*/
dispose() {
for (const resourceName of this.loadedResources) {
this.disposedResources.add(resourceName);
}
this.loadedResources.clear();
this.loadResourceCallbacks.clear();
this.loadResourcePromises.clear();
}
/**
* Dispose specific resources
*/
unloadResourcesList(resourcesList) {
for (const resource of resourcesList) {
this.disposedResources.add(resource.name);
this.loadedResources.delete(resource.name);
this.loadResourceCallbacks.delete(resource.name);
this.loadResourcePromises.delete(resource.name);
}
}
/**
* @returns {ResourceKind[]}
*/
getResourceKinds() {
return ['fake-heavy-resource'];
return ['fake-resource-kind-for-testing-only'];
}
}

View File

@@ -0,0 +1,347 @@
// @ts-check
/**
* Tests for gdjs.ResourceLoader.
*/
describe('gdjs.ResourceLoader', () => {
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
/** @returns {LayoutData} */
const createSceneData = (name, usedResources) => {
return {
r: 0,
v: 0,
b: 0,
mangledName: name,
name,
objects: [],
layers: [],
instances: [],
behaviorsSharedData: [],
stopSoundsOnStartup: false,
title: '',
variables: [],
usedResources,
};
};
/** @type {{layouts?: LayoutData[], resources?: ResourcesData}} */
const gameSettingsWithThreeScenes = {
layouts: [
createSceneData('Scene1', [
{ name: 'scene1-resource1.png' },
{ name: 'scene1-resource2.png' },
]),
createSceneData('Scene2', [
{ name: 'scene2-resource1.png' },
{ name: 'shared-resource.png' },
]),
createSceneData('Scene3', [
{ name: 'scene3-resource1.png' },
{ name: 'shared-resource.png' },
]),
],
resources: {
resources: [
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-resource1.png',
metadata: '',
file: 'scene1-resource1.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-resource2.png',
metadata: '',
file: 'scene1-resource2.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene2-resource1.png',
metadata: '',
file: 'scene2-resource1.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene3-resource1.png',
metadata: '',
file: 'scene3-resource1.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'shared-resource.png',
metadata: '',
file: 'shared-resource.png',
userAdded: true,
},
],
},
};
it('should load first scene resources, then others in background', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Initially, no scene assets should be loaded
expect(resourceLoader.areSceneAssetsLoaded('Scene1')).to.be(false);
expect(resourceLoader.areSceneAssetsLoaded('Scene2')).to.be(false);
expect(resourceLoader.areSceneAssetsLoaded('Scene3')).to.be(false);
expect(resourceLoader.areSceneAssetsReady('Scene1')).to.be(false);
expect(resourceLoader.areSceneAssetsReady('Scene2')).to.be(false);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(false);
// Start loading first scene and background loading
runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1');
// Scene1 resources should be pending download
expect(
mockedResourceManager.isResourceDownloadPending('scene1-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending('scene1-resource2.png')
).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene1')).to.be(false);
// Mark Scene1 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png');
await delay(10);
// Scene1 should now be ready
expect(resourceLoader.areSceneAssetsLoaded('Scene1')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene1')).to.be(true);
// Background loading should have started for Scene2
await delay(20); // Wait for background loading to start
expect(
mockedResourceManager.isResourceDownloadPending('scene2-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending('shared-resource.png')
).to.be(true);
// Mark Scene2 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene2-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('shared-resource.png');
await delay(10);
// Scene2 should now be loaded
expect(resourceLoader.areSceneAssetsLoaded('Scene2')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene2')).to.be(false);
// Scene 2 resources can be processed so the scene is fully ready:
resourceLoader.loadAndProcessSceneResources('Scene2');
await delay(10);
expect(resourceLoader.areSceneAssetsLoaded('Scene2')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene2')).to.be(true);
// Scene 3 resources are not loaded nor processed yet:
expect(resourceLoader.areSceneAssetsLoaded('Scene3')).to.be(false);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(false);
// Background loading should have started for Scene3
await delay(20); // Wait for background loading to start
expect(
mockedResourceManager.isResourceDownloadPending('scene3-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending('shared-resource.png')
// shared-resource.png should already be loaded, so not pending:
).to.be(false);
// Mark Scene3 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene3-resource1.png');
await delay(10);
// Scene3 should now be loaded
expect(resourceLoader.areSceneAssetsLoaded('Scene3')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(false);
// Scene3 resources can be processed so the scene is fully ready:
resourceLoader.loadAndProcessSceneResources('Scene3');
await delay(10);
expect(resourceLoader.areSceneAssetsLoaded('Scene3')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(true);
});
it('should unload only resources unique to the unloaded scene', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Load all resources for all scenes
resourceLoader.loadGlobalAndFirstSceneResources('Scene1', () => {});
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png');
resourceLoader.loadAndProcessSceneResources('Scene2');
mockedResourceManager.markPendingResourcesAsLoaded('scene2-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('shared-resource.png');
await delay(10);
resourceLoader.loadAndProcessSceneResources('Scene3');
mockedResourceManager.markPendingResourcesAsLoaded('scene3-resource1.png');
await delay(10);
// Verify all resources are loaded
expect(resourceLoader.areSceneAssetsReady('Scene1')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene2')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(true);
// Verify no resources are disposed initially
expect(
mockedResourceManager.isResourceDisposed('scene1-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('scene1-resource2.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('scene2-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('scene3-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('shared-resource.png')
).to.be(false);
// Simulate Scene2 and Scene3 being loaded/active by marking Scene1 as unloaded
// while Scene2 will be the new scene
resourceLoader.unloadSceneResources({
unloadedSceneName: 'Scene1',
newSceneName: 'Scene2',
});
// Only Scene1-specific resources should be disposed
// shared-resource.png should NOT be disposed because it's used in Scene2 and Scene3
expect(
mockedResourceManager.isResourceDisposed('scene1-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDisposed('scene1-resource2.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDisposed('scene2-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('scene3-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('shared-resource.png')
).to.be(false);
// Scene1 should be marked as not loaded
expect(resourceLoader.areSceneAssetsLoaded('Scene1')).to.be(false);
expect(resourceLoader.areSceneAssetsReady('Scene1')).to.be(false);
// Other scenes should still be loaded
expect(resourceLoader.areSceneAssetsLoaded('Scene2')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene2')).to.be(true);
expect(resourceLoader.areSceneAssetsLoaded('Scene3')).to.be(true);
expect(resourceLoader.areSceneAssetsReady('Scene3')).to.be(true);
});
it('should unload shared resources only when no other scene uses them', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Load all resources for all scenes
resourceLoader.loadAllResources(() => {});
await delay(10);
// First, unload Scene2 (which shares resources with Scene3)
resourceLoader.unloadSceneResources({
unloadedSceneName: 'Scene2',
newSceneName: 'Scene3',
});
// Only Scene2-specific resources should be disposed
// shared-resource.png should NOT be disposed because it's still used in Scene3
expect(
mockedResourceManager.isResourceDisposed('scene2-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDisposed('shared-resource.png')
).to.be(false);
// Now unload Scene3 (which also uses shared-resource.png)
resourceLoader.unloadSceneResources({
unloadedSceneName: 'Scene3',
newSceneName: 'Scene1',
});
// Now shared-resource.png should be disposed because no loaded scene uses it
expect(
mockedResourceManager.isResourceDisposed('scene3-resource1.png')
).to.be(true);
expect(
mockedResourceManager.isResourceDisposed('shared-resource.png')
).to.be(true);
// Scene1 resources should still be loaded
expect(
mockedResourceManager.isResourceDisposed('scene1-resource1.png')
).to.be(false);
expect(
mockedResourceManager.isResourceDisposed('scene1-resource2.png')
).to.be(false);
});
it('should handle background scene loading progress correctly', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Initially progress should be 0
expect(resourceLoader.getSceneLoadingProgress('Scene1')).to.be(0);
expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(0);
// Start loading first scene
runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png');
await delay(10);
// Progress should still be 0 until resources start loading
expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(0);
// Mark first resource as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene2-resource1.png');
await delay(10);
// Progress should be partial (1 out of 2 resources)
console.log(resourceLoader.getSceneLoadingProgress('Scene2'));
expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(0.5);
// Mark second resource as loaded
mockedResourceManager.markPendingResourcesAsLoaded('shared-resource.png');
await delay(10);
// Progress should be complete (1.0)
expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(1);
});
});

View File

@@ -6,7 +6,7 @@
describe('gdjs.SceneStack', () => {
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
const createSene = (name, usedResources) => {
const createSceneData = (name, usedResources) => {
return {
r: 0,
v: 0,
@@ -24,10 +24,13 @@ describe('gdjs.SceneStack', () => {
};
};
/** @type {{layouts?: LayoutData[], resources?: ResourcesData}} */
const gameSettings = {
layouts: [
createSene('Scene 1', []),
createSene('Scene 2', [{ name: 'base/tests-utils/assets/64x64.jpg' }]),
createSceneData('Scene 1', []),
createSceneData('Scene 2', [
{ name: 'base/tests-utils/assets/64x64.jpg' },
]),
],
resources: {
resources: [
@@ -43,11 +46,11 @@ describe('gdjs.SceneStack', () => {
};
it('should support pushing, replacing and popping scenes', async () => {
//@ts-ignore
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettings);
let sceneStack = runtimeGame._sceneStack;
// Async asset loading is not tested here.
await runtimeGame._resourcesLoader.loadAllResources(() => {});
const resourcesLoader = runtimeGame.getResourceLoader();
await resourcesLoader.loadAllResources(() => {});
// Set up some scene callbacks.
/** @type gdjs.RuntimeScene | null */
@@ -146,38 +149,39 @@ describe('gdjs.SceneStack', () => {
gdjs._unregisterCallback(onRuntimeSceneResumed);
});
/** @type {{layouts?: LayoutData[], resources?: ResourcesData}} */
const gameSettingsWithHeavyResource = {
layouts: [
createSene('Scene 1', [{ name: 'fake-heavy-resource1.png' }]),
createSene('Scene 2', [{ name: 'fake-heavy-resource2.png' }]),
createSene('Scene 3', [{ name: 'fake-heavy-resource3.png' }]),
createSene('Scene 4', [{ name: 'fake-heavy-resource4.png' }]),
createSceneData('Scene 1', [{ name: 'fake-heavy-resource1.png' }]),
createSceneData('Scene 2', [{ name: 'fake-heavy-resource2.png' }]),
createSceneData('Scene 3', [{ name: 'fake-heavy-resource3.png' }]),
createSceneData('Scene 4', [{ name: 'fake-heavy-resource4.png' }]),
],
resources: {
resources: [
{
kind: 'fake-heavy-resource',
kind: 'fake-resource-kind-for-testing-only',
name: 'fake-heavy-resource1.png',
metadata: '',
file: 'fake-heavy-resource1.png',
userAdded: true,
},
{
kind: 'fake-heavy-resource',
kind: 'fake-resource-kind-for-testing-only',
name: 'fake-heavy-resource2.png',
metadata: '',
file: 'fake-heavy-resource2.png',
userAdded: true,
},
{
kind: 'fake-heavy-resource',
kind: 'fake-resource-kind-for-testing-only',
name: 'fake-heavy-resource3.png',
metadata: '',
file: 'fake-heavy-resource3.png',
userAdded: true,
},
{
kind: 'fake-heavy-resource',
kind: 'fake-resource-kind-for-testing-only',
name: 'fake-heavy-resource4.png',
metadata: '',
file: 'fake-heavy-resource4.png',
@@ -189,11 +193,11 @@ describe('gdjs.SceneStack', () => {
it('can start a layout when all its assets are already downloaded', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
//@ts-ignore
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithHeavyResource);
runtimeGame._resourcesLoader._resourceManagersMap.set(
//@ts-ignore
'fake-heavy-resource',
const resourcesLoader = runtimeGame.getResourceLoader();
resourcesLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
let sceneStack = runtimeGame._sceneStack;
@@ -299,11 +303,11 @@ describe('gdjs.SceneStack', () => {
it('can start a layout while assets loading and wait them to finish', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
//@ts-ignore
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithHeavyResource);
runtimeGame._resourcesLoader._resourceManagersMap.set(
//@ts-ignore
'fake-heavy-resource',
const resourcesLoader = runtimeGame.getResourceLoader();
resourcesLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
let sceneStack = runtimeGame._sceneStack;
@@ -404,11 +408,10 @@ describe('gdjs.SceneStack', () => {
it("can start a layout which assets loading didn't stated yet and wait them to finish", async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
//@ts-ignore
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithHeavyResource);
runtimeGame._resourcesLoader._resourceManagersMap.set(
//@ts-ignore
'fake-heavy-resource',
const resourcesLoader = runtimeGame.getResourceLoader();
resourcesLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
let sceneStack = runtimeGame._sceneStack;

View File

@@ -666,6 +666,11 @@ interface Project {
[Ref] ObjectsContainer GetObjects();
[Ref] ResourcesManager GetResourcesManager();
void SetSceneResourcesPreloading([Const] DOMString resourcesPreloading);
[Const, Ref] DOMString GetSceneResourcesPreloading();
void SetSceneResourcesUnloading([Const] DOMString resourcesUnloading);
[Const, Ref] DOMString GetSceneResourcesUnloading();
void SerializeTo([Ref] SerializerElement element);
void UnserializeFrom([Const, Ref] SerializerElement element);
@@ -1002,6 +1007,11 @@ interface Layout {
void SetStopSoundsOnStartup(boolean enable);
boolean StopSoundsOnStartup();
void SetResourcesPreloading([Const] DOMString resourcesPreloading);
[Const, Ref] DOMString GetResourcesPreloading();
void SetResourcesUnloading([Const] DOMString resourcesUnloading);
[Const, Ref] DOMString GetResourcesUnloading();
};
interface ExternalEvents {
@@ -1049,10 +1059,13 @@ interface Effect {
void SetDoubleParameter([Const] DOMString name, double value);
double GetDoubleParameter([Const] DOMString name);
boolean HasDoubleParameter([Const] DOMString name);
void SetStringParameter([Const] DOMString name, [Const] DOMString value);
[Const, Ref] DOMString GetStringParameter([Const] DOMString name);
boolean HasStringParameter([Const] DOMString name);
void SetBooleanParameter([Const] DOMString name, boolean value);
boolean GetBooleanParameter([Const] DOMString name);
boolean HasBooleanParameter([Const] DOMString name);
[Const, Ref] MapStringDouble GetAllDoubleParameters();
[Const, Ref] MapStringString GetAllStringParameters();
[Const, Ref] MapStringBoolean GetAllBooleanParameters();
@@ -1136,6 +1149,17 @@ interface LayersContainer {
void UnserializeLayersFrom([Const, Ref] SerializerElement element);
};
interface PropertyDescriptorChoice {
void PropertyDescriptorChoice([Const] DOMString value, [Const] DOMString label);
[Const, Ref] DOMString GetValue();
[Const, Ref] DOMString GetLabel();
};
interface VectorPropertyDescriptorChoice {
unsigned long size();
[Const, Ref] PropertyDescriptorChoice at(unsigned long index);
};
interface PropertyDescriptor {
void PropertyDescriptor([Const] DOMString propValue);
@@ -1149,6 +1173,8 @@ interface PropertyDescriptor {
[Const, Ref] DOMString GetDescription();
[Ref] PropertyDescriptor SetGroup([Const] DOMString label);
[Const, Ref] DOMString GetGroup();
[Ref] PropertyDescriptor AddChoice([Const] DOMString value, [Const] DOMString label);
[Const, Ref] VectorPropertyDescriptorChoice GetChoices();
[Ref] PropertyDescriptor AddExtraInfo([Const] DOMString type);
[Ref] PropertyDescriptor SetExtraInfo([Const, Ref] VectorString info);
[Ref] VectorString GetExtraInfo();

View File

@@ -481,6 +481,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice;
// Customize some functions implementation thanks to WRAPPED_* macros
// The original names will be reconstructed in the js file (see postjs.js)

View File

@@ -475,7 +475,7 @@ class RuntimeObject {
}
/** @param {RuntimeScene} runtimeScene */
onDestroyFromScene(runtimeScene) {
onDeletedFromScene(runtimeScene) {
// Note: these mocks don't support behaviors nor layers or effects.
this.destroyCallbacks.forEach((c) => c());
@@ -789,7 +789,7 @@ class RuntimeScene {
}
//Notify the object it was removed from the scene
obj.onDestroyFromScene(this);
obj.onDeletedFromScene(this);
}
getObjects(objectName) {

View File

@@ -619,6 +619,10 @@ export class Project extends EmscriptenObject {
getVariables(): VariablesContainer;
getObjects(): ObjectsContainer;
getResourcesManager(): ResourcesManager;
setSceneResourcesPreloading(resourcesPreloading: string): void;
getSceneResourcesPreloading(): string;
setSceneResourcesUnloading(resourcesUnloading: string): void;
getSceneResourcesUnloading(): string;
serializeTo(element: SerializerElement): void;
unserializeFrom(element: SerializerElement): void;
getWholeProjectDiagnosticReport(): WholeProjectDiagnosticReport;
@@ -823,6 +827,10 @@ export class Layout extends EmscriptenObject {
unserializeFrom(project: Project, element: SerializerElement): void;
setStopSoundsOnStartup(enable: boolean): void;
stopSoundsOnStartup(): boolean;
setResourcesPreloading(resourcesPreloading: string): void;
getResourcesPreloading(): string;
setResourcesUnloading(resourcesUnloading: string): void;
getResourcesUnloading(): string;
}
export class ExternalEvents extends EmscriptenObject {
@@ -858,10 +866,13 @@ export class Effect extends EmscriptenObject {
isFolded(): boolean;
setDoubleParameter(name: string, value: number): void;
getDoubleParameter(name: string): number;
hasDoubleParameter(name: string): boolean;
setStringParameter(name: string, value: string): void;
getStringParameter(name: string): string;
hasStringParameter(name: string): boolean;
setBooleanParameter(name: string, value: boolean): void;
getBooleanParameter(name: string): boolean;
hasBooleanParameter(name: string): boolean;
getAllDoubleParameters(): MapStringDouble;
getAllStringParameters(): MapStringString;
getAllBooleanParameters(): MapStringBoolean;
@@ -936,6 +947,17 @@ export class LayersContainer extends EmscriptenObject {
unserializeLayersFrom(element: SerializerElement): void;
}
export class PropertyDescriptorChoice extends EmscriptenObject {
constructor(value: string, label: string);
getValue(): string;
getLabel(): string;
}
export class VectorPropertyDescriptorChoice extends EmscriptenObject {
size(): number;
at(index: number): PropertyDescriptorChoice;
}
export class PropertyDescriptor extends EmscriptenObject {
constructor(propValue: string);
setValue(value: string): PropertyDescriptor;
@@ -948,6 +970,8 @@ export class PropertyDescriptor extends EmscriptenObject {
getDescription(): string;
setGroup(label: string): PropertyDescriptor;
getGroup(): string;
addChoice(value: string, label: string): PropertyDescriptor;
getChoices(): VectorPropertyDescriptorChoice;
addExtraInfo(type: string): PropertyDescriptor;
setExtraInfo(info: VectorString): PropertyDescriptor;
getExtraInfo(): VectorString;

View File

@@ -9,10 +9,13 @@ declare class gdEffect {
isFolded(): boolean;
setDoubleParameter(name: string, value: number): void;
getDoubleParameter(name: string): number;
hasDoubleParameter(name: string): boolean;
setStringParameter(name: string, value: string): void;
getStringParameter(name: string): string;
hasStringParameter(name: string): boolean;
setBooleanParameter(name: string, value: boolean): void;
getBooleanParameter(name: string): boolean;
hasBooleanParameter(name: string): boolean;
getAllDoubleParameters(): gdMapStringDouble;
getAllStringParameters(): gdMapStringString;
getAllBooleanParameters(): gdMapStringBoolean;

View File

@@ -32,6 +32,10 @@ declare class gdLayout {
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
setStopSoundsOnStartup(enable: boolean): void;
stopSoundsOnStartup(): boolean;
setResourcesPreloading(resourcesPreloading: string): void;
getResourcesPreloading(): string;
setResourcesUnloading(resourcesUnloading: string): void;
getResourcesUnloading(): string;
delete(): void;
ptr: number;
};

View File

@@ -107,6 +107,10 @@ declare class gdProject {
getVariables(): gdVariablesContainer;
getObjects(): gdObjectsContainer;
getResourcesManager(): gdResourcesManager;
setSceneResourcesPreloading(resourcesPreloading: string): void;
getSceneResourcesPreloading(): string;
setSceneResourcesUnloading(resourcesUnloading: string): void;
getSceneResourcesUnloading(): string;
serializeTo(element: gdSerializerElement): void;
unserializeFrom(element: gdSerializerElement): void;
getWholeProjectDiagnosticReport(): gdWholeProjectDiagnosticReport;

View File

@@ -11,6 +11,8 @@ declare class gdPropertyDescriptor {
getDescription(): string;
setGroup(label: string): gdPropertyDescriptor;
getGroup(): string;
addChoice(value: string, label: string): gdPropertyDescriptor;
getChoices(): gdVectorPropertyDescriptorChoice;
addExtraInfo(type: string): gdPropertyDescriptor;
setExtraInfo(info: gdVectorString): gdPropertyDescriptor;
getExtraInfo(): gdVectorString;

View File

@@ -0,0 +1,8 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdPropertyDescriptorChoice {
constructor(value: string, label: string): void;
getValue(): string;
getLabel(): string;
delete(): void;
ptr: number;
};

View File

@@ -0,0 +1,7 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdVectorPropertyDescriptorChoice {
size(): number;
at(index: number): gdPropertyDescriptorChoice;
delete(): void;
ptr: number;
};

View File

@@ -105,6 +105,8 @@ declare class libGDevelop {
EffectsContainer: Class<gdEffectsContainer>;
Layer: Class<gdLayer>;
LayersContainer: Class<gdLayersContainer>;
PropertyDescriptorChoice: Class<gdPropertyDescriptorChoice>;
VectorPropertyDescriptorChoice: Class<gdVectorPropertyDescriptorChoice>;
PropertyDescriptor: Class<gdPropertyDescriptor>;
MeasurementUnit: Class<gdMeasurementUnit>;
MeasurementBaseUnit: Class<gdMeasurementBaseUnit>;

View File

@@ -1,2 +1,3 @@
# Prettier has troubles handling the generic typing with flow in this file.
src/Utils/UseOptimisticState.js
src/Utils/UseOptimisticState.js
src/Utils/Sha256.js

View File

@@ -441,6 +441,17 @@
}
},
"rst": {
"ai-generated-event-move-handle": {
"gradient-color-1": {
"value": "#9932cc"
},
"gradient-color-2": {
"value": "#fa7ff1"
},
"gradient-color-3": {
"value": "#9932cc"
}
},
"move-handle": {
"background-color": {
"value": "#494949"

View File

@@ -136,12 +136,26 @@ const getQuotaOrCreditsText = ({
>
<div>
{isMobile ? (
<Trans>{quota.max - quota.current} free requests left</Trans>
increaseQuotaOffering === 'subscribe' ? (
<Trans>{quota.max - quota.current} trial requests left</Trans>
) : (
<Trans>{quota.max - quota.current} free requests left</Trans>
)
) : quota.period === '30days' ? (
increaseQuotaOffering === 'subscribe' ? (
<Trans>
{quota.max - quota.current} free trial requests left this month
</Trans>
) : (
<Trans>
{quota.max - quota.current} of {quota.max} free requests left this
month
</Trans>
)
) : quota.period === '1day' ? (
<Trans>{quota.max - quota.current} free requests left today</Trans>
) : (
<Trans>
{quota.max - quota.current} of {quota.max} free requests left this
month
</Trans>
<Trans>{quota.max - quota.current} free requests left</Trans>
)}
</div>
</Tooltip>

View File

@@ -2,7 +2,10 @@
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { I18n } from '@lingui/react';
import { type RenderEditorContainerPropsWithRef } from '../MainFrame/EditorContainers/BaseEditor';
import {
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
} from '../MainFrame/EditorContainers/BaseEditor';
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import Paper from '../UI/Paper';
import { AiRequestChat, type AiRequestChatInterface } from './AiRequestChat';
@@ -48,6 +51,7 @@ import {
} from '../Utils/Analytics/EventSender';
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { prepareAiUserContent } from './PrepareAiUserContent';
const gd: libGDevelop = global.gd;
@@ -131,7 +135,9 @@ const useProcessFunctionCalls = ({
string,
Array<EditorFunctionCallResult>
) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|}) => {
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
@@ -473,7 +479,9 @@ type Props = {|
| 'none',
|}
) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
@@ -487,7 +495,9 @@ export type AskAiEditorInterface = {|
objectWithContext: ObjectWithContext
) => void,
onSceneObjectsDeleted: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
startNewChat: () => void,
|};
@@ -714,11 +724,17 @@ export const AskAiEditor = React.memo<Props>(
setSendingAiRequest(null, true);
const preparedAiUserContent = await prepareAiUserContent({
getAuthorizationHeader,
userId: profile.id,
simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
});
const aiRequest = await createAiRequest(getAuthorizationHeader, {
userRequest: userRequest,
userId: profile.id,
gameProjectJson: simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
...preparedAiUserContent,
payWithCredits,
gameId: project ? project.getProjectUuid() : null,
fileMetadata,
@@ -863,13 +879,19 @@ export const AskAiEditor = React.memo<Props>(
)
: null;
const preparedAiUserContent = await prepareAiUserContent({
getAuthorizationHeader,
userId: profile.id,
simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
});
const aiRequest: AiRequest = await retryIfFailed({ times: 2 }, () =>
addMessageToAiRequest(getAuthorizationHeader, {
userId: profile.id,
aiRequestId: selectedAiRequestId,
functionCallOutputs,
gameProjectJson: simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
...preparedAiUserContent,
payWithCredits,
userMessage,
})

View File

@@ -0,0 +1,233 @@
// @flow
import axios from 'axios';
import { retryIfFailed } from '../Utils/RetryIfFailed';
import {
createAiUserContentPresignedUrls,
type AiUserContentPresignedUrlsResult,
} from '../Utils/GDevelopServices/Generation';
import jsSHA from '../Utils/Sha256';
type UploadInfo = {
uploadedAt: number,
userRelativeKey: string | null,
};
const makeUploadCache = ({
minimalContentLength,
}: {|
minimalContentLength: number | null,
|}) => {
const uploadCacheByHash: {
[string]: UploadInfo,
} = {};
return {
getUserRelativeKey: (hash: string | null) => {
if (!hash) {
return null;
}
return (
(uploadCacheByHash[hash] && uploadCacheByHash[hash].userRelativeKey) ||
null
);
},
storeUpload: (hash: string | null, uploadInfo: UploadInfo) => {
if (!hash) return;
uploadCacheByHash[hash] = uploadInfo;
},
shouldUpload: ({
hash,
contentLength,
}: {|
hash: string | null,
contentLength: number,
|}) => {
if (!hash) {
// No hash, so no content to upload.
return false;
}
if (minimalContentLength && contentLength < minimalContentLength) {
// The content is too small to be uploaded.
return false;
}
if (
uploadCacheByHash[hash] &&
uploadCacheByHash[hash].uploadedAt > Date.now() - 1000 * 60 * 30
) {
// The content was already uploaded recently (and recently enough so that it has not expired in such a short time).
// We don't need to upload it again.
return false;
}
// The content was not uploaded, or not recently: we'll upload it now.
return true;
},
};
};
const projectSpecificExtensionsSummaryUploadCache = makeUploadCache({
minimalContentLength: null, // Always upload the project specific extensions summary.
});
const gameProjectJsonUploadCache = makeUploadCache({
minimalContentLength: 10 * 1000, // Roughly 10KB.
});
const computeSha256 = (payload: string): string => {
const shaObj = new jsSHA('SHA-256', 'TEXT', { encoding: 'UTF8' });
shaObj.update(payload);
return shaObj.getHash('HEX');
};
/**
* Prepare the user content to be used by the AI.
* It either uploads the content (and avoid uploading it again for a while)
* so that the request will just refer to the key where it's stored, or
* return the content so it's sent as part of the request itself (if it's small enough).
*/
export const prepareAiUserContent = async ({
getAuthorizationHeader,
userId,
simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
}: {|
getAuthorizationHeader: () => Promise<string>,
userId: string,
simplifiedProjectJson: string | null,
projectSpecificExtensionsSummaryJson: string | null,
|}) => {
// Hash the contents, if provided, to then upload it only once (as long as the hash stays
// the same, no need to re-upload it for a while).
// If the content is not provided, no hash is computed because there is no content to upload.
const startTime = Date.now();
const gameProjectJsonHash = simplifiedProjectJson
? computeSha256(simplifiedProjectJson)
: null;
const projectSpecificExtensionsSummaryJsonHash = projectSpecificExtensionsSummaryJson
? computeSha256(projectSpecificExtensionsSummaryJson)
: null;
const endTime = Date.now();
console.info(
`Hash of simplified project json and project specific extensions summary json took ${(
endTime - startTime
).toFixed(2)}ms`
);
const shouldUploadProjectSpecificExtensionsSummary = projectSpecificExtensionsSummaryUploadCache.shouldUpload(
{
hash: projectSpecificExtensionsSummaryJsonHash,
contentLength: projectSpecificExtensionsSummaryJson
? projectSpecificExtensionsSummaryJson.length
: 0,
}
);
const shouldUploadGameProjectJson = gameProjectJsonUploadCache.shouldUpload({
hash: gameProjectJsonHash,
contentLength: simplifiedProjectJson ? simplifiedProjectJson.length : 0,
});
if (
shouldUploadGameProjectJson ||
shouldUploadProjectSpecificExtensionsSummary
) {
const startTime = Date.now();
const {
gameProjectJsonSignedUrl,
gameProjectJsonUserRelativeKey,
projectSpecificExtensionsSummaryJsonSignedUrl,
projectSpecificExtensionsSummaryJsonUserRelativeKey,
}: AiUserContentPresignedUrlsResult = await retryIfFailed(
{ times: 3 },
() =>
createAiUserContentPresignedUrls(getAuthorizationHeader, {
userId,
gameProjectJsonHash: shouldUploadGameProjectJson
? gameProjectJsonHash
: null,
projectSpecificExtensionsSummaryJsonHash: shouldUploadProjectSpecificExtensionsSummary
? projectSpecificExtensionsSummaryJsonHash
: null,
})
);
const uploadedAt = Date.now();
await Promise.all([
gameProjectJsonSignedUrl
? retryIfFailed({ times: 3 }, () =>
axios.put(gameProjectJsonSignedUrl, simplifiedProjectJson, {
headers: {
'Content-Type': 'application/json',
},
// Allow any arbitrary large file to be sent
maxContentLength: Infinity,
})
).then(() => {
gameProjectJsonUploadCache.storeUpload(gameProjectJsonHash, {
uploadedAt,
userRelativeKey: gameProjectJsonUserRelativeKey || null,
});
})
: null,
projectSpecificExtensionsSummaryJsonSignedUrl
? retryIfFailed({ times: 3 }, () =>
axios.put(
projectSpecificExtensionsSummaryJsonSignedUrl,
projectSpecificExtensionsSummaryJson,
{
headers: {
'Content-Type': 'application/json',
},
// Allow any arbitrary large file to be sent
maxContentLength: Infinity,
}
)
).then(() => {
projectSpecificExtensionsSummaryUploadCache.storeUpload(
projectSpecificExtensionsSummaryJsonHash,
{
uploadedAt,
userRelativeKey:
projectSpecificExtensionsSummaryJsonUserRelativeKey || null,
}
);
})
: null,
]);
const endTime = Date.now();
console.info(
`Upload of ${[
shouldUploadGameProjectJson ? 'simplified project' : null,
shouldUploadProjectSpecificExtensionsSummary
? 'project specific extensions summary'
: null,
]
.filter(Boolean)
.join(' and ')} took ${(endTime - startTime).toFixed(2)}ms`
);
}
// Get the key at which the content was uploaded, if it was uploaded.
// If not, the content will be sent as part of the request instead of the upload key.
const gameProjectJsonUserRelativeKey = gameProjectJsonUploadCache.getUserRelativeKey(
gameProjectJsonHash
);
const projectSpecificExtensionsSummaryJsonUserRelativeKey = projectSpecificExtensionsSummaryUploadCache.getUserRelativeKey(
projectSpecificExtensionsSummaryJsonHash
);
return {
gameProjectJsonUserRelativeKey,
gameProjectJson: gameProjectJsonUserRelativeKey
? null
: simplifiedProjectJson,
projectSpecificExtensionsSummaryJsonUserRelativeKey,
projectSpecificExtensionsSummaryJson: projectSpecificExtensionsSummaryJsonUserRelativeKey
? null
: projectSpecificExtensionsSummaryJson,
};
};

View File

@@ -10,6 +10,7 @@ import {
import { type EventsGenerationResult } from '../EditorFunctions';
import { makeSimplifiedProjectBuilder } from '../EditorFunctions/SimplifiedProject/SimplifiedProject';
import { prepareAiUserContent } from './PrepareAiUserContent';
const gd: libGDevelop = global.gd;
@@ -40,21 +41,24 @@ export const useGenerateEvents = ({ project }: {| project: ?gdProject |}) => {
if (!profile) throw new Error('User should be authenticated.');
const simplifiedProjectBuilder = makeSimplifiedProjectBuilder(gd);
const simplifiedProjectJson = JSON.stringify(
simplifiedProjectBuilder.getSimplifiedProject(project, {})
);
const projectSpecificExtensionsSummaryJson = JSON.stringify(
simplifiedProjectBuilder.getProjectSpecificExtensionsSummary(project)
);
const preparedAiUserContent = await prepareAiUserContent({
getAuthorizationHeader,
userId: profile.id,
simplifiedProjectJson,
projectSpecificExtensionsSummaryJson,
});
const createResult = await retryIfFailed({ times: 2 }, () =>
createAiGeneratedEvent(getAuthorizationHeader, {
userId: profile.id,
partialGameProjectJson: JSON.stringify(
simplifiedProjectBuilder.getSimplifiedProject(project, {
scopeToScene: sceneName,
}),
null,
2
),
projectSpecificExtensionsSummaryJson,
...preparedAiUserContent,
sceneName,
eventsDescription,
extensionNamesList,

View File

@@ -27,13 +27,20 @@ import ThreeDotsMenu from '../../UI/CustomSvgIcons/ThreeDotsMenu';
import useAlertDialog from '../../UI/Alert/useAlertDialog';
import ExtensionInstallDialog from '../ExtensionStore/ExtensionInstallDialog';
import { getIDEVersion } from '../../Version';
import InAppTutorialContext from '../../InAppTutorial/InAppTutorialContext';
export const useExtensionUpdateAlertDialog = () => {
const { showConfirmation } = useAlertDialog();
const { currentlyRunningInAppTutorial } = React.useContext(
InAppTutorialContext
);
return async (
project: gdProject,
behaviorShortHeader: BehaviorShortHeader
): Promise<boolean> => {
if (currentlyRunningInAppTutorial) {
return false;
}
return await showConfirmation({
title: t`Extension update`,
message:

View File

@@ -49,11 +49,15 @@ import ErrorBoundary from '../UI/ErrorBoundary';
import type { ObjectFolderOrObjectWithContext } from '../ObjectsList/EnumerateObjectFolderOrObject';
import LoaderModal from '../UI/LoaderModal';
import { AssetStoreNavigatorContext } from './AssetStoreNavigator';
import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
const isDev = Window.isDev();
export const useExtensionUpdateAlertDialog = () => {
const { showConfirmation, showDeleteConfirmation } = useAlertDialog();
const { currentlyRunningInAppTutorial } = React.useContext(
InAppTutorialContext
);
return async ({
project,
outOfDateExtensionShortHeaders,
@@ -61,6 +65,9 @@ export const useExtensionUpdateAlertDialog = () => {
project: gdProject,
outOfDateExtensionShortHeaders: Array<ExtensionShortHeader>,
|}): Promise<string> => {
if (currentlyRunningInAppTutorial) {
return 'skip';
}
const breakingChanges = new Map<
ExtensionShortHeader,
Array<ExtensionChange>

View File

@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import { mapFor } from '../Utils/MapFor';
import { mapFor, mapVector } from '../Utils/MapFor';
import { type Schema, type Instance } from '.';
import { type ResourceKind } from '../ResourcesList/ResourceSource';
import { type Field } from '.';
@@ -112,14 +112,19 @@ const createField = (
};
} else if (valueType === 'choice') {
// Choice is a "string" (with a selector for the user in the UI)
const choices = property
const choices = mapVector(property.getChoices(), choice => ({
value: choice.getValue(),
label: choice.getLabel(),
}));
const deprecatedChoices = property
.getExtraInfo()
.toJSArray()
.map(value => ({ value, label: value }));
return {
name,
valueType: 'string',
getChoices: () => choices,
getChoices: () => [...choices, ...deprecatedChoices],
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)

View File

@@ -52,6 +52,10 @@ export type ValueFieldCommonProperties = {|
onEditButtonClick?: () => void,
getValueFromDisplayedValue?: string => string,
getDisplayedValueFromValue?: string => string,
// Only used for effects for now:
advanced?: boolean,
defaultValue?: string | number | boolean,
|};
// "Primitive" value fields are "simple" fields.

View File

@@ -9,6 +9,7 @@ import {
type EventsGenerationOptions,
type AssetSearchAndInstallOptions,
type AssetSearchAndInstallResult,
type SceneEventsOutsideEditorChanges,
} from '.';
export type EditorFunctionCallResult =
@@ -35,7 +36,9 @@ export type ProcessEditorFunctionCallsOptions = {|
generateEvents: (
options: EventsGenerationOptions
) => Promise<EventsGenerationResult>,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
ensureExtensionInstalled: (options: {|
extensionName: string,
|}) => Promise<void>,

View File

@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import { getInstancesInLayoutForLayer } from '../Utils/Layout';
import { mapFor } from '../Utils/MapFor';
import { mapFor, mapVector } from '../Utils/MapFor';
import { SafeExtractor } from '../Utils/SafeExtractor';
import { serializeToJSObject } from '../Utils/Serializer';
import { type AiGeneratedEvent } from '../Utils/GDevelopServices/Generation';
@@ -112,6 +112,11 @@ export type EditorCallbacks = {|
) => void,
|};
export type SceneEventsOutsideEditorChanges = {|
scene: gdLayout,
newOrChangedAiGeneratedEventIds: Set<string>,
|};
/**
* A function that does something in the editor on the given project.
*/
@@ -132,7 +137,9 @@ export type EditorFunction = {|
generateEvents: (
options: EventsGenerationOptions
) => Promise<EventsGenerationResult>,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
ensureExtensionInstalled: (options: {|
extensionName: string,
|}) => Promise<void>,
@@ -251,7 +258,10 @@ const makeShortTextForNamedProperty = (
const choices =
type.toLowerCase() === 'choice'
? property.getExtraInfo().toJSArray()
? [
...mapVector(property.getChoices(), choice => choice.getValue()),
...property.getExtraInfo().toJSArray(),
]
: null;
const information = [
type,
@@ -2233,7 +2243,10 @@ const addSceneEvents: EditorFunction = {
changes,
aiGeneratedEvent.id
);
onSceneEventsModifiedOutsideEditor(scene);
onSceneEventsModifiedOutsideEditor({
scene,
newOrChangedAiGeneratedEventIds: new Set([aiGeneratedEvent.id]),
});
const resultMessage =
aiGeneratedEvent.resultMessage ||

View File

@@ -1,5 +1,5 @@
// @flow
import { mapFor } from '../Utils/MapFor';
import { mapFor, mapVector } from '../Utils/MapFor';
import { type Schema } from '../CompactPropertiesEditor';
import { type ResourceKind } from '../ResourcesList/ResourceSource';
import flatten from 'lodash/flatten';
@@ -49,30 +49,40 @@ export const enumerateEffectsMetadata = (
const getLabel = () => propertyLabel;
const getDescription = () => propertyDescription;
const getExtraDescription = () => parameterName;
const advanced = property.isAdvanced();
const defaultValue = property.getValue();
if (valueType === 'number') {
return {
name: parameterName,
valueType: 'number',
getValue: (effect: gdEffect) =>
effect.getDoubleParameter(parameterName),
effect.hasDoubleParameter(parameterName)
? effect.getDoubleParameter(parameterName)
: parseFloat(defaultValue) || 0,
setValue: (effect: gdEffect, newValue: number) =>
effect.setDoubleParameter(parameterName, newValue),
getLabel,
getDescription,
getExtraDescription,
advanced,
defaultValue: parseFloat(defaultValue) || 0,
};
} else if (valueType === 'boolean') {
return {
name: parameterName,
valueType: 'boolean',
getValue: (effect: gdEffect) =>
effect.getBooleanParameter(parameterName),
effect.hasBooleanParameter(parameterName)
? effect.getBooleanParameter(parameterName)
: defaultValue === '1',
setValue: (effect: gdEffect, newValue: boolean) =>
effect.setBooleanParameter(parameterName, newValue),
getLabel,
getDescription,
getExtraDescription,
advanced,
defaultValue: defaultValue === 'true',
};
} else if (valueType === 'resource') {
// Resource is a "string" (with a selector in the UI)
@@ -84,41 +94,57 @@ export const enumerateEffectsMetadata = (
valueType: 'resource',
resourceKind: kind,
getValue: (effect: gdEffect) =>
effect.getStringParameter(parameterName),
effect.hasStringParameter(parameterName)
? effect.getStringParameter(parameterName)
: defaultValue,
setValue: (effect: gdEffect, newValue: string) =>
effect.setStringParameter(parameterName, newValue),
getLabel,
getDescription,
getExtraDescription,
advanced,
defaultValue,
};
} else if (valueType === 'color') {
return {
name: parameterName,
valueType: 'color',
getValue: (effect: gdEffect) =>
effect.getStringParameter(parameterName),
effect.hasStringParameter(parameterName)
? effect.getStringParameter(parameterName)
: defaultValue,
setValue: (effect: gdEffect, newValue: string) =>
effect.setStringParameter(parameterName, newValue),
getLabel,
getDescription,
getExtraDescription,
advanced,
defaultValue,
};
} else if (valueType === 'choice') {
const choices = property
const choices = mapVector(property.getChoices(), choice => ({
value: choice.getValue(),
label: choice.getLabel(),
}));
const deprecatedChoices = property
.getExtraInfo()
.toJSArray()
.map(value => ({ value, label: value }));
return {
name: parameterName,
valueType: 'string',
getChoices: () => choices,
getChoices: () => [...choices, ...deprecatedChoices],
getValue: (effect: gdEffect) =>
effect.getStringParameter(parameterName),
effect.hasStringParameter(parameterName)
? effect.getStringParameter(parameterName)
: defaultValue,
setValue: (effect: gdEffect, newValue: string) =>
effect.setStringParameter(parameterName, newValue),
getLabel,
getDescription,
getExtraDescription,
advanced,
defaultValue,
};
} else {
console.error(

View File

@@ -53,6 +53,8 @@ import PasteIcon from '../UI/CustomSvgIcons/Clipboard';
import CopyIcon from '../UI/CustomSvgIcons/Copy';
import { type ConnectDragSource } from 'react-dnd';
import ResponsiveFlatButton from '../UI/ResponsiveFlatButton';
import { Accordion, AccordionHeader, AccordionBody } from '../UI/Accordion';
import { type Field } from '../CompactPropertiesEditor';
const gd: libGDevelop = global.gd;
@@ -132,14 +134,12 @@ const Effect = React.forwardRef(
ref
) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const preferences = React.useContext(PreferencesContext);
const showEffectParameterNames =
preferences.values.showEffectParameterNames;
const setShowEffectParameterNames = preferences.setShowEffectParameterNames;
const forceUpdate = useForceUpdate();
const isClipboardContainingEffects = Clipboard.has(EFFECTS_CLIPBOARD_KIND);
const renameEffect = React.useCallback(
@@ -190,6 +190,42 @@ const Effect = React.forwardRef(
effectType
);
const parametersSchema = effectMetadata && effectMetadata.parametersSchema;
const basicPropertiesSchema = React.useMemo(
() =>
parametersSchema
? parametersSchema.filter(param => param.valueType && !param.advanced)
: [],
[parametersSchema]
);
const advancedPropertiesSchema = React.useMemo(
() =>
parametersSchema
? parametersSchema.filter(param => param.valueType && param.advanced)
: [],
[parametersSchema]
);
const areAdvancedPropertiesModified = React.useMemo(
() => {
return advancedPropertiesSchema.some((field: Field) => {
const name = field.valueType ? field.name : null;
if (!name) return false;
const current =
field.valueType === 'number'
? effect.getDoubleParameter(name)
: field.valueType === 'boolean'
? effect.getBooleanParameter(name)
: effect.getStringParameter(name);
const defaultValue = field.valueType ? field.defaultValue : null;
return current !== defaultValue;
});
},
[advancedPropertiesSchema, effect]
);
return (
<I18n>
{({ i18n }) => (
@@ -286,33 +322,61 @@ const Effect = React.forwardRef(
/>
<Spacer />
</div>
{effectType && (
{effectMetadata && (
<Line expand noMargin>
<Column expand>
{effectMetadata ? (
<React.Fragment>
<Line>
<BackgroundText>
<MarkdownText source={effectMetadata.description} />
</BackgroundText>
</Line>
<PropertiesEditor
key={effect.getEffectType()}
instances={[effect]}
schema={effectMetadata.parametersSchema}
project={project}
resourceManagementProps={resourceManagementProps}
renderExtraDescriptionText={
showEffectParameterNames
? parameterName =>
i18n._(
t`Property name in events: \`${parameterName}\` `
)
: undefined
}
/>
</React.Fragment>
) : null}
<Line>
<BackgroundText>
<MarkdownText source={effectMetadata.description} />
</BackgroundText>
</Line>
<PropertiesEditor
key={effect.getEffectType() + '-basic'}
instances={[effect]}
schema={basicPropertiesSchema}
project={project}
resourceManagementProps={resourceManagementProps}
renderExtraDescriptionText={
showEffectParameterNames
? parameterName =>
i18n._(
t`Property name in events: \`${parameterName}\` `
)
: undefined
}
/>
{advancedPropertiesSchema.length > 0 && (
<Accordion
defaultExpanded={areAdvancedPropertiesModified}
noMargin
>
<AccordionHeader noMargin>
<Text size="sub-title">
<Trans>Advanced properties</Trans>
</Text>
</AccordionHeader>
<AccordionBody disableGutters>
<Column expand noMargin>
<PropertiesEditor
key={effect.getEffectType() + '-advanced'}
instances={[effect]}
schema={advancedPropertiesSchema}
project={project}
resourceManagementProps={resourceManagementProps}
renderExtraDescriptionText={
showEffectParameterNames
? parameterName =>
i18n._(
t`Property name in events: \`${parameterName}\` `
)
: undefined
}
/>
</Column>
</AccordionBody>
</Accordion>
)}
<Spacer />
</Column>
</Line>

View File

@@ -27,6 +27,7 @@ export const eventsTreeWithSearchResults = 'with-search-results';
export const dropIndicator = 'drop-indicator';
export const cantDropIndicator = 'cant-drop-indicator';
export const handle = 'move-handle';
export const aiGeneratedEventHandle = 'ai-generated-event-move-handle';
export const linkContainer = 'link-container';

View File

@@ -16,6 +16,7 @@ import {
eventsTree,
eventsTreeWithSearchResults,
handle,
aiGeneratedEventHandle,
icon,
} from './ClassNames';
import {
@@ -134,6 +135,7 @@ type EventsContainerProps = {|
windowSize: WindowSizeType,
idPrefix: string,
highlightedAiGeneratedEventIds: Set<string>,
|};
/**
@@ -151,6 +153,7 @@ const EventContainer = (props: EventsContainerProps) => {
onEventContextMenu,
projectScopedContainersAccessor,
onUpdate,
highlightedAiGeneratedEventIds,
} = props;
const forceUpdate = useForceUpdate();
const containerRef = React.useRef<?HTMLDivElement>(null);
@@ -204,7 +207,16 @@ const EventContainer = (props: EventsContainerProps) => {
>
{!!EventComponent && (
<div style={styles.eventComponentContainer}>
{props.connectDragSource(<div className={handle} />)}
{props.connectDragSource(
<div
className={classNames({
[handle]: true,
[aiGeneratedEventHandle]: highlightedAiGeneratedEventIds.has(
event.getAiGeneratedEventId()
),
})}
/>
)}
<div style={styles.container}>
<EventComponent
project={project}
@@ -348,6 +360,8 @@ type EventsTreeProps = {|
preferences: Preferences,
tutorials: ?Array<Tutorial>,
highlightedAiGeneratedEventIds: Set<string>,
|};
export type EventsTreeInterface = {|
@@ -770,6 +784,9 @@ const EventsTree = React.forwardRef<EventsTreeProps, EventsTreeInterface>(
connectDragSource={connectDragSource}
windowSize={props.windowSize}
idPrefix={`event-${node.relativeNodePath.join('-')}`}
highlightedAiGeneratedEventIds={
props.highlightedAiGeneratedEventIds
}
/>
{draggedNode && (
<DropContainer

View File

@@ -0,0 +1,46 @@
// @flow
import * as React from 'react';
type Props = {|
isActive: boolean,
|};
type Output = {|
addNewAiGeneratedEventIds: (ids: Set<string>) => void,
highlightedAiGeneratedEventIds: Set<string>,
|};
export const useHighlightedAiGeneratedEvent = ({ isActive }: Props): Output => {
const [
highlightedAiGeneratedEventIds,
setHighlightedAiGeneratedEventIds,
] = React.useState<Set<string>>(() => new Set());
const resetHighlightsOnNextAddition = React.useRef(false);
React.useEffect(
() => {
// When the events sheet is "navigated away", then next time there is a generated event,
// the previously highlighted event(s) will be reset (because we consider the user saw them).
if (!isActive) {
resetHighlightsOnNextAddition.current = true;
}
},
[isActive]
);
const addNewAiGeneratedEventIds = React.useCallback((ids: Set<string>) => {
setHighlightedAiGeneratedEventIds(prev => {
const newSet = new Set(
resetHighlightsOnNextAddition.current ? new Set() : prev
);
ids.forEach(id => newSet.add(id));
resetHighlightsOnNextAddition.current = false;
return newSet;
});
}, []);
return { addNewAiGeneratedEventIds, highlightedAiGeneratedEventIds };
};

View File

@@ -119,6 +119,7 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even
import LocalVariablesDialog from '../VariablesList/LocalVariablesDialog';
import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog';
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
import { useHighlightedAiGeneratedEvent } from './UseHighlightedAiGeneratedEvent';
const gd: libGDevelop = global.gd;
@@ -165,6 +166,7 @@ type ComponentProps = {|
tutorials: ?Array<Tutorial>,
leaderboardsManager: ?LeaderboardState,
shortcutMap: ShortcutMap,
highlightedAiGeneratedEventIds: Set<string>,
|};
type State = {|
@@ -1876,6 +1878,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
tutorials,
hotReloadPreviewButtonProps,
windowSize,
highlightedAiGeneratedEventIds,
} = this.props;
if (!project) return null;
@@ -2031,6 +2034,9 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
fontSize={preferences.values.eventsSheetZoomLevel}
preferences={preferences}
tutorials={tutorials}
highlightedAiGeneratedEventIds={
highlightedAiGeneratedEventIds
}
/>
)}
{this.state.showSearchPanel && (
@@ -2227,10 +2233,14 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
}
}
type OutOfEditorChanges = {|
newOrChangedAiGeneratedEventIds: Set<string>,
|};
export type EventsSheetInterface = {|
updateToolbar: () => void,
onResourceExternallyChanged: ({| identifier: string |}) => void,
onEventsModifiedOutsideEditor: () => void,
onEventsModifiedOutsideEditor: (changes: OutOfEditorChanges) => void,
|};
// EventsSheet is a wrapper so that the component can use multiple
@@ -2242,6 +2252,11 @@ const EventsSheet = (props, ref) => {
onEventsModifiedOutsideEditor,
}));
const {
highlightedAiGeneratedEventIds,
addNewAiGeneratedEventIds,
} = useHighlightedAiGeneratedEvent({ isActive: props.isActive });
const component = React.useRef<?EventsSheetComponentWithoutHandle>(null);
const updateToolbar = () => {
if (component.current) component.current.updateToolbar();
@@ -2250,7 +2265,8 @@ const EventsSheet = (props, ref) => {
if (component.current)
component.current.onResourceExternallyChanged(resourceInfo);
};
const onEventsModifiedOutsideEditor = () => {
const onEventsModifiedOutsideEditor = (changes: OutOfEditorChanges) => {
addNewAiGeneratedEventIds(changes.newOrChangedAiGeneratedEventIds);
if (component.current) component.current.onEventsModifiedOutsideEditor();
};
@@ -2269,6 +2285,7 @@ const EventsSheet = (props, ref) => {
leaderboardsManager={leaderboardsManager}
shortcutMap={shortcutMap}
windowSize={windowSize}
highlightedAiGeneratedEventIds={highlightedAiGeneratedEventIds}
{...props}
/>
);

View File

@@ -28,6 +28,11 @@ export type EditorContainerExtraProps = {|
storageProviders?: Array<StorageProvider>,
|};
export type SceneEventsOutsideEditorChanges = {|
scene: gdLayout,
newOrChangedAiGeneratedEventIds: Set<string>,
|};
export type RenderEditorContainerProps = {|
isActive: boolean,
projectItemName: ?string,
@@ -166,7 +171,9 @@ export type RenderEditorContainerProps = {|
onSceneObjectsDeleted: (scene: gdLayout) => void,
// Events editing
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (
changes: SceneEventsOutsideEditorChanges
) => void,
onExtractAsExternalLayout: (name: string) => void,
onExtractAsEventBasedObject: (

View File

@@ -3,6 +3,7 @@ import * as React from 'react';
import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
} from './BaseEditor';
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
import {
@@ -106,7 +107,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
// No thing to be done.
}
onSceneEventsModifiedOutsideEditor(scene: gdLayout) {
onSceneEventsModifiedOutsideEditor(changes: SceneEventsOutsideEditorChanges) {
// No thing to be done.
}

View File

@@ -6,6 +6,7 @@ import Debugger from '../../Debugger';
import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
} from './BaseEditor';
import SubscriptionChecker, {
type SubscriptionCheckerInterface,
@@ -61,7 +62,7 @@ export class DebuggerEditorContainer extends React.Component<
// No thing to be done.
}
onSceneEventsModifiedOutsideEditor(scene: gdLayout) {
onSceneEventsModifiedOutsideEditor(changes: SceneEventsOutsideEditorChanges) {
// No thing to be done.
}

View File

@@ -5,6 +5,7 @@ import { sendEventsExtractedAsFunction } from '../../Utils/Analytics/EventSender
import {
type RenderEditorContainerProps,
type RenderEditorContainerPropsWithRef,
type SceneEventsOutsideEditorChanges,
} from './BaseEditor';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
@@ -57,9 +58,13 @@ export class EventsEditorContainer extends React.Component<RenderEditorContainer
// No thing to be done.
}
onSceneEventsModifiedOutsideEditor(scene: gdLayout) {
if (this.getLayout() === scene) {
if (this.editor) this.editor.onEventsModifiedOutsideEditor();
onSceneEventsModifiedOutsideEditor(changes: SceneEventsOutsideEditorChanges) {
if (this.getLayout() === changes.scene) {
if (this.editor)
this.editor.onEventsModifiedOutsideEditor({
newOrChangedAiGeneratedEventIds:
changes.newOrChangedAiGeneratedEventIds,
});
}
}

Some files were not shown because too many files have changed in this diff Show More