Compare commits

..

41 Commits

Author SHA1 Message Date
Florian Rival
08f00bb893 Use vitest for GDJS browser tests 2025-07-01 16:49:49 +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
Florian Rival
a4c2778b8d Fix keyboard avoidance on touchscreens for Ask AI tab (#7670)
* Fix keyboard avoidance on touchscreens for Ask AI tab
* Add placementId for in-app analytics

Don't show in changelog
2025-06-24 15:43:08 +02:00
D8H
f26e56c3bf Fix resource not loading when a custom object has both a variant and children overriding (#7668)
Don't show in changelog
2025-06-24 14:25:58 +02:00
Florian Rival
f5f9944fc4 Upgade m4pro.medium and XCode for CircleCI macOS build (#7669)
* This reduces total build time by ~40%.
2025-06-24 11:45:29 +02:00
D8H
9467caf1e9 Fix changing of variant not being applied at hot-reload (#7666) 2025-06-24 10:41:57 +02:00
Florian Rival
00376f39d5 Fix formatting and macOS CI build uploads
Don't show in changelog
2025-06-24 09:47:54 +02:00
Florian Rival
40b6a34dc5 Improve AI agent style
Don't show in changelog
2025-06-24 01:18:03 +02:00
Florian Rival
17d2b8c2c2 Improve the style of the text input for the AI agent/chat
Don't show in changelog
2025-06-23 23:41:29 +02:00
Florian Rival
935af42d23 Fix error when positioning instances in the AI agent
+ tentative fix for failing upload of macOS builds on the CI

Don't show in changelog
2025-06-23 20:05:26 +02:00
Florian Rival
d4a8d468cb Improve AI scene instance creation (#7667) 2025-06-23 18:52:39 +02:00
Florian Rival
b16099aee0 Bump newIDE version 2025-06-23 12:51:33 +02:00
github-actions[bot]
c17b918a43 Update translations [skip ci] (#7663)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-06-23 09:42:28 +02:00
D8H
d58e8c7ef9 Fix hot-reload when custom objects are in an external layout (#7664) 2025-06-21 13:51:37 +02:00
Florian Rival
ddd6b6e3a8 Open a new AI chat when clicking on button to try the AI agent 2025-06-21 12:22:54 +02:00
Florian Rival
e629c132ea Improve opening of scene and events tab for AI
Don't show in changelog
2025-06-20 18:01:44 +02:00
github-actions[bot]
b80e03f153 Update translations [skip ci] (#7660)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-06-20 15:46:09 +02:00
Florian Rival
11e36ff3f1 Fix CI not failing if macOS build upload failed 2025-06-20 15:16:03 +02:00
Florian Rival
22de356413 Improve animation names support for AI and add retry for macOS build deployment
Don't show in changelog
2025-06-20 12:56:46 +02:00
Florian Rival
caefa04fbe Give AI the names of the animations of animatable objects
Don't show in changelog
2025-06-20 11:53:13 +02:00
Florian Rival
cf2e7d67d7 Improve robustness of AI event generation
Don't show in changelog
2025-06-20 11:34:12 +02:00
D8H
685e444b2d Comply variants when an extension is updated for an asset (#7661)
- Don't show in changelog
2025-06-20 10:31:42 +02:00
Florian Rival
a9c1045afd Avoid extra scrollbar in dialog to create a project with AI
Don't show in changelog
2025-06-18 14:56:48 +02:00
Florian Rival
24e0d37583 Improve draggable behavior condition descriptions 2025-06-18 12:56:38 +02:00
Florian Rival
d44997d372 Fix AI agent unable to change boolean properties 2025-06-18 00:09:45 +02:00
Florian Rival
062aa888f8 Disable AI agent chat when started for another project
Don't show in changelog
2025-06-17 18:07:26 +02:00
github-actions[bot]
de4c2ae4ad Update translations (#7633)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-06-17 17:24:50 +02:00
Florian Rival
29ad7308c3 Introduce an experimental AI agent (#7659)
- This is an AI agent that takes a request and takes actions on a project: it can create scenes, find and create objects, add, remove behaviors, modify them, put instances on the scene, create or modify events, and more to come (layers, setup leaderboards, etc...).
- It's still in beta and there is room for improvement on many things, but is already useful for prototyping and learning - beginners notably are able to see what the AI can do and learn the concepts of GDevelop. For intermediate and power users, it's useful to try new things, or get things done while working on something else.
- Experiment with it and always make backup of your project before starting - in the future restoration points will be added to go back to a previous state if the result is not good or broken.
2025-06-17 16:25:03 +02:00
D8H
19b21c280e Fix a case where neither the variant nor the children are shown in the object editor (#7658)
- Don't show in changelog
2025-06-16 19:06:55 +02:00
190 changed files with 2134 additions and 737 deletions

View File

@@ -18,13 +18,13 @@ jobs:
# Build the **entire** app for macOS (including the GDevelop.js library).
build-macos:
macos:
xcode: 14.2.0
resource_class: macos.m1.large.gen1
xcode: 16.4.0
resource_class: m4pro.medium
steps:
- checkout
# Install Rosetta for AWS CLI and disable TSO to speed up S3 uploads (https://support.circleci.com/hc/en-us/articles/19334402064027-Troubleshooting-slow-uploads-to-S3-for-jobs-using-an-m1-macOS-resource-class)
- macos/install-rosetta
- run: sudo sysctl net.inet.tcp.tso=0
# - run: sudo sysctl net.inet.tcp.tso=0
# Install a recent version of npm to workaround a notarization issue because of a symlink made by npm: https://github.com/electron-userland/electron-builder/issues/7755
# Node.js v20.14.0 comes with npm v10.7.0.
@@ -88,13 +88,35 @@ jobs:
- store_artifacts:
path: newIDE/electron-app/dist
# Upload artifacts (AWS)
- run:
name: Deploy to S3 (specific commit)
command: export PATH=~/.local/bin:$PATH && aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/
command: |
export PATH=~/.local/bin:$PATH
for i in 1 2 3 4 5 6 7; do
aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/commit/$(git rev-parse HEAD)/ && break
echo "Retry $i failed... retrying in 10 seconds"
sleep 10
done
if [ $i -eq 7 ]; then
echo "All retries for deployment failed!" >&2
exit 1
fi
- run:
name: Deploy to S3 (latest)
command: export PATH=~/.local/bin:$PATH && aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/latest/
command: |
export PATH=~/.local/bin:$PATH
for i in 1 2 3 4 5 6 7; do
aws s3 sync newIDE/electron-app/dist s3://gdevelop-releases/$(git rev-parse --abbrev-ref HEAD)/latest/ && break
echo "Retry $i failed... retrying in 10 seconds"
sleep 10
done
if [ $i -eq 7 ]; then
echo "All retries for deployment failed!" >&2
exit 1
fi
# Build the app for Linux (using a pre-built GDevelop.js library).
build-linux:
@@ -368,7 +390,7 @@ jobs:
npm -v
Remove-Item package-lock.json
$Env:REQUIRES_EXACT_LIBGD_JS_VERSION = "true"
npm install
@@ -425,10 +447,10 @@ jobs:
$Env:GD_SIGNTOOL_THUMBPRINT = ''
$Env:GD_SIGNTOOL_SUBJECT_NAME = ''
$Env:GD_SIGNTOOL_SUBJECT_NAME = ''
$Env:CSC_LINK = ''
$Env:CSC_KEY_PASSWORD = ''
node scripts/build.js --skip-app-build --win appx --publish=never
@@ -445,16 +467,16 @@ jobs:
name: Install AWS CLI
command: |
# Install the CLI for the current user
pip install --quiet --upgrade --user awscli
# Add the user-Scripts dir to PATH for this step and the next.
$binDir = (python -m site --user-base) + "\Scripts"
$Env:Path += ";$binDir"
# Sanity check:
aws --version
aws --version
# Upload artifacts (S3)
- run:

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");
@@ -235,7 +235,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddAction("SetAngle",
_("Angle"),
_("Change the angle of rotation of an object (in degrees)."),
_("Change the angle of rotation of an object (in degrees). For "
"3D objects, this is the rotation around the Z axis."),
_("the angle"),
_("Angle"),
"res/actions/direction24_black.png",
@@ -250,7 +251,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddAction("Rotate",
_("Rotate"),
_("Rotate an object, clockwise if the speed is positive, "
"counterclockwise otherwise."),
"counterclockwise otherwise. For 3D objects, this is the "
"rotation around the Z axis."),
_("Rotate _PARAM0_ at speed _PARAM1_ deg/second"),
_("Angle"),
"res/actions/rotate24_black.png",
@@ -634,7 +636,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddCondition("Angle",
_("Angle"),
_("Compare the angle of the specified object."),
_("Compare the angle, in degrees, of the specified object. "
"For 3D objects, this is the angle around the Z axis."),
_("the angle (in degrees)"),
_("Angle"),
"res/conditions/direction24_black.png",
@@ -835,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"))
@@ -1268,7 +1270,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddExpression("Angle",
_("Angle"),
_("Current angle, in degrees, of the object"),
_("Current angle, in degrees, of the object. For 3D "
"objects, this is the angle around the Z axis."),
_("Angle"),
"res/actions/direction_black.png")
.AddParameter("object", _("Object"));
@@ -1571,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

@@ -250,25 +250,28 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
}
const auto &eventsBasedObject = project->GetEventsBasedObject(GetType());
if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
if (IsForcedToOverrideEventsBasedObjectChildrenConfiguration()) {
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
auto &configuration = GetChildObjectConfiguration(childObject->GetName());
configuration.ExposeResources(worker);
}
}
else if (eventsBasedObject.GetVariants().HasVariantNamed(variantName)) {
for (auto &childObject : eventsBasedObject.GetVariants()
.GetVariant(variantName)
.GetObjects()
.GetObjects()) {
childObject->GetConfiguration().ExposeResources(worker);
}
} else if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
auto &configuration = GetChildObjectConfiguration(childObject->GetName());
configuration.ExposeResources(worker);
}
} else {
if (variantName.empty() ||
!eventsBasedObject.GetVariants().HasVariantNamed(variantName)) {
for (auto &childObject :
eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) {
childObject->GetConfiguration().ExposeResources(worker);
}
} else {
for (auto &childObject : eventsBasedObject.GetVariants()
.GetVariant(variantName)
.GetObjects()
.GetObjects()) {
childObject->GetConfiguration().ExposeResources(worker);
}
for (auto &childObject :
eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) {
childObject->GetConfiguration().ExposeResources(worker);
}
}
}

View File

@@ -78,6 +78,15 @@ public:
variantName = variantName_;
}
/**
* Legacy events-based objects don't have any instance in their default
* variant since there wasn't a graphical editor at the time. In this case,
* the editor doesn't allow to choose a variant, but a variant may have stayed
* after a user rolled back the extension. This variant must be ignored.
*
* @return true when its events-based object doesn't have any initial
* instance.
*/
bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const;
bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const {

View File

@@ -365,6 +365,8 @@ class GD_CORE_API InitialInstance {
* the same initial instance between serialization.
*/
InitialInstance& ResetPersistentUuid();
const gd::String& GetPersistentUuid() const { return persistentUuid; }
///@}
private:

View File

@@ -34,7 +34,6 @@ namespace gdjs {
objectData: gdjs.Object3DData & gdjs.CustomObjectConfiguration
) {
super(parent, objectData);
this._renderer.reinitialize(this, parent);
}
protected override _createRender() {

View File

@@ -44,10 +44,7 @@ namespace gdjs {
) {
this._object = object;
this._isContainerDirty = true;
const layer = parent.getLayer('');
if (layer) {
layer.getRenderer().add3DRendererObject(this._threeGroup);
}
this._threeGroup.clear();
}
_updateThreeGroup() {

View File

@@ -14,6 +14,7 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
effects: [],
content: {},
childrenContent: {},
isInnerAreaFollowingParentSize: false,
});
runtimeScene.addObject(customObject);
customObject.setPosition(500, 250);

View File

@@ -35,25 +35,32 @@ void DeclareDraggableBehaviorExtension(gd::PlatformExtension& extension) {
std::make_shared<DraggableBehavior>(),
std::shared_ptr<gd::BehaviorsSharedData>());
aut.AddCondition("Dragged",
_("Being dragged"),
_("Check if the object is being dragged."),
_("_PARAM0_ is being dragged"),
_("Draggable"),
"CppPlatform/Extensions/draggableicon24.png",
"CppPlatform/Extensions/draggableicon16.png")
aut.AddCondition(
"Dragged",
_("Being dragged"),
_("Check if the object is being dragged. This means the mouse button "
"or touch is pressed on it. When the mouse button or touch is "
"released, the object is no longer being considered dragged (use "
"the condition \"Was just dropped\" to check when the dragging is "
"ending)."),
_("_PARAM0_ is being dragged"),
_("Draggable"),
"CppPlatform/Extensions/draggableicon24.png",
"CppPlatform/Extensions/draggableicon16.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "Draggable")
.SetFunctionName("IsDragged");
aut.AddCondition("Dropped",
_("Was just dropped"),
_("Check if the object was just dropped after being dragged."),
_("_PARAM0_ was just dropped"),
_("Draggable"),
"CppPlatform/Extensions/draggableicon24.png",
"CppPlatform/Extensions/draggableicon16.png")
aut.AddCondition(
"Dropped",
_("Was just dropped"),
_("Check if the object was just dropped after being dragged (the "
"mouse button or touch was just released this frame)."),
_("_PARAM0_ was just dropped"),
_("Draggable"),
"CppPlatform/Extensions/draggableicon24.png",
"CppPlatform/Extensions/draggableicon16.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "Draggable")

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

@@ -14,6 +14,7 @@ namespace gdjs {
animatable?: SpriteAnimationData[];
variant: string;
childrenContent: { [objectName: string]: ObjectConfiguration & any };
isInnerAreaFollowingParentSize: boolean;
};
/**
@@ -111,9 +112,19 @@ namespace gdjs {
name: '',
};
}
// Legacy events-based objects don't have any instance in their default
// variant since there wasn't a graphical editor at the time.
// In this case, the editor doesn't allow to choose a variant, but a
// variant may have stayed after a user rolled back the extension.
// This variant must be ignored to match what the editor shows.
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
eventsBasedObjectData.defaultVariant.instances.length == 0;
let usedVariantData: EventsBasedObjectVariantData =
eventsBasedObjectData.defaultVariant;
if (customObjectData.variant) {
if (
customObjectData.variant &&
!isForcedToOverrideEventsBasedObjectChildrenConfiguration
) {
for (
let variantIndex = 0;
variantIndex < eventsBasedObjectData.variants.length;
@@ -154,10 +165,12 @@ namespace gdjs {
override reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
super.reinitialize(objectData);
this._initializeFromObjectData(objectData);
this._reinitializeRenderer();
this._initializeFromObjectData(objectData);
// The generated code calls the onCreated super implementation at the end.
// When changing the variant, the instance is like a new instance.
// We call again `onCreated` at the end, like done by the constructor
// the first time it's created.
this.onCreated();
}
@@ -172,6 +185,34 @@ namespace gdjs {
newObjectData.animatable || []
);
}
if (oldObjectData.variant !== newObjectData.variant) {
const width = this.getWidth();
const height = this.getHeight();
const hasInnerAreaChanged =
oldObjectData.isInnerAreaFollowingParentSize &&
this._instanceContainer._initialInnerArea &&
this._innerArea &&
(this._instanceContainer._initialInnerArea.min[0] !==
this._innerArea.min[0] ||
this._instanceContainer._initialInnerArea.min[1] !==
this._innerArea.min[1] ||
this._instanceContainer._initialInnerArea.max[0] !==
this._innerArea.max[0] ||
this._instanceContainer._initialInnerArea.max[1] !==
this._innerArea.max[1]);
this._reinitializeRenderer();
this._initializeFromObjectData(newObjectData);
// The generated code calls the onCreated super implementation at the end.
this.onCreated();
// Keep the custom size
if (hasInnerAreaChanged) {
this.setWidth(width);
this.setHeight(height);
}
}
return true;
}

View File

@@ -8,7 +8,6 @@ namespace gdjs {
objectData: ObjectData & CustomObjectConfiguration
) {
super(parent, objectData);
this.getRenderer().reinitialize(this, parent);
}
protected override _createRender(): gdjs.CustomRuntimeObject2DRenderer {

View File

@@ -24,7 +24,7 @@ namespace gdjs {
*
* @see gdjs.CustomRuntimeObject._innerArea
**/
private _initialInnerArea: {
_initialInnerArea: {
min: [float, float, float];
max: [float, float, float];
} | null = null;
@@ -47,6 +47,9 @@ namespace gdjs {
}
addLayer(layerData: LayerData) {
if (this._layers.containsKey(layerData.name)) {
return;
}
const layer = new gdjs.RuntimeCustomObjectLayer(layerData, this);
this._layers.put(layerData.name, layer);
this._orderedLayers.push(layer);
@@ -71,6 +74,10 @@ namespace gdjs {
this.onDestroyFromScene(this._parent);
}
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
!eventsBasedObjectVariantData.name &&
eventsBasedObjectVariantData.instances.length == 0;
this._setOriginalInnerArea(eventsBasedObjectVariantData);
// Registering objects
@@ -83,7 +90,8 @@ namespace gdjs {
// The children configuration override only applies to the default variant.
if (
customObjectData.childrenContent &&
!eventsBasedObjectVariantData.name
(!eventsBasedObjectVariantData.name ||
isForcedToOverrideEventsBasedObjectChildrenConfiguration)
) {
this.registerObject({
...childObjectData,

View File

@@ -811,6 +811,8 @@ namespace gdjs {
this._objectsCtor = new Hashtable();
this._allInstancesList = [];
this._instancesRemoved = [];
this._layersCameraCoordinates = {};
this._initialBehaviorSharedData = new Hashtable();
}
}
}

View File

@@ -222,7 +222,9 @@ namespace gdjs {
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading: ' +
error.message,
error.message +
'\n' +
error.stack,
});
}
})
@@ -472,13 +474,24 @@ namespace gdjs {
newExternalLayoutData.associatedLayout
);
const oldObjectDataList =
HotReloader.resolveCustomObjectConfigurations(
oldProjectData,
oldLayoutData ? oldLayoutData.objects : []
);
const newObjectDataList =
HotReloader.resolveCustomObjectConfigurations(
newProjectData,
newLayoutData ? newLayoutData.objects : []
);
sceneStack._stack.forEach((runtimeScene) => {
this._hotReloadRuntimeSceneInstances(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
oldLayoutData ? oldLayoutData.objects : [],
newLayoutData ? newLayoutData.objects : [],
oldObjectDataList,
newObjectDataList,
oldExternalLayoutData.instances,
newExternalLayoutData.instances,
runtimeScene

View File

@@ -50,12 +50,7 @@ namespace gdjs {
) {
this._object = object;
this._isContainerDirty = true;
const layer = parent.getLayer('');
if (layer) {
layer
.getRenderer()
.addRendererObject(this._pixiContainer, object.getZOrder());
}
this._pixiContainer.removeChildren();
}
getRendererObject() {

View File

@@ -229,7 +229,7 @@ declare interface EventsBasedObjectVariantData extends InstanceContainerData {
/**
* A value shared by every object instances.
*
* @see gdjs.CustomRuntimeObjectInstanceContainer._originalInnerArea
* @see gdjs.CustomRuntimeObjectInstanceContainer._initialInnerArea
**/
_initialInnerArea: {
min: [float, float, float];

View File

@@ -12,9 +12,8 @@ npm install
Then launch tests:
```bash
npm run test:watch # This will use Chrome Headless
npm run test:watch # Runs tests in a headless browser using Vitest
npm run test-benchmark:watch # This will also run benchmarks
npm run test:firefox:watch # To run tests using Firefox
```
> ⚠️ If you're working on GDJS or extensions, make sure to have the development version of GDevelop running so that changes in GDJS or extension files are rebuilt (or run `npm run build` in `GDJS/`, but better run GDevelop so that any changes are watched).
@@ -23,7 +22,7 @@ npm run test:firefox:watch # To run tests using Firefox
### Unit tests
Tests are launched using Chrome. You need Chrome installed to run them. You can change the browser by modifying the package.json "test" command and install the appropriate karma package.
Tests are launched using [Vitest](https://vitest.dev/) in a headless browser. You can change the browser by editing the `vitest.config.js` file.
Tests are located in the **tests** folder for the game engine, or directly in the folder of the tested extensions.

View File

@@ -1,6 +1,6 @@
describe('gdjs.Force', function() {
it('benchmark setting angle and length', function(){
this.timeout(20000);
it('benchmark setting angle and length', function(){
vi.setTimeout(20000);
var layer = new gdjs.Force();
const benchmarkSuite = makeBenchmarkSuite();

View File

@@ -5,7 +5,7 @@ describe('gdjs.Layer', function() {
var runtimeScene = new gdjs.RuntimeScene(runtimeGame);
it('benchmark convertCoords and convertInverseCoords', function() {
this.timeout(30000);
vi.setTimeout(30000);
var layer = new gdjs.Layer(
{ name: 'My layer',
visibility: true,

View File

@@ -1,6 +1,6 @@
describe('gdjs.Polygon', function() {
it('benchmark gdjs.Polygon.collisionTest between two polygons', function() {
this.timeout(20000);
vi.setTimeout(20000);
var rect1 = gdjs.Polygon.createRectangle(32, 40);
var rect2 = gdjs.Polygon.createRectangle(32, 40);
var rect3 = gdjs.Polygon.createRectangle(32, 40);

View File

@@ -1,8 +1,8 @@
describe('gdjs.RuntimeObject', function() {
const runtimeScene = new gdjs.RuntimeScene(null);
it('benchmark getAABB of rotated vs non rotated objects', function(){
this.timeout(20000);
it('benchmark getAABB of rotated vs non rotated objects', function(){
vi.setTimeout(20000);
var object = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
object.getWidth = function() { return 10; };
object.getHeight = function() { return 20; };
@@ -25,8 +25,8 @@ describe('gdjs.RuntimeObject', function() {
console.log(benchmarkSuite.run());
});
it('benchmark getAABB of rotated vs non rotated objects, with non default center', function(){
this.timeout(20000);
it('benchmark getAABB of rotated vs non rotated objects, with non default center', function(){
vi.setTimeout(20000);
var object = new gdjs.RuntimeObject(runtimeScene, {name: "obj1", type: "", behaviors: [], effects: []});
object.getWidth = function() { return 10; };
object.getHeight = function() { return 20; };

View File

@@ -65,7 +65,7 @@ describe('gdjs.SpriteRuntimeObject', function () {
});
it('benchmark getAABB of rotated vs non rotated sprite, with custom hitboxes, origin and center', function () {
this.timeout(20000);
vi.setTimeout(20000);
const object = makeSpriteRuntimeObjectWithCustomHitBox(runtimeScene);
const benchmarkSuite = makeBenchmarkSuite({

View File

@@ -1,6 +1,6 @@
describe('gdjs.VariablesContainer', function() {
it('benchmark get', function() {
this.timeout(20000);
vi.setTimeout(20000);
var container = new gdjs.VariablesContainer();
const benchmarkSuite = makeBenchmarkSuite();

View File

@@ -5,16 +5,10 @@
"main": "index.js",
"private": true,
"scripts": {
"test": "karma start --browsers ChromeHeadless --single-run",
"test:watch": "karma start --browsers ChromeHeadless",
"test-benchmark": "karma start --browsers ChromeHeadless --single-run --enableBenchmarks",
"test-benchmark:watch": "karma start --browsers ChromeHeadless --enableBenchmarks",
"test:firefox": "karma start --browsers Firefox --single-run",
"test:firefox:watch": "karma start --browsers Firefox",
"test:chrome": "karma start --browsers Chrome --single-run",
"test:chrome:watch": "karma start --browsers Chrome",
"test:edge": "karma start --browsers EdgeHeadless --single-run",
"test:edge:watch": "karma start --browsers EdgeHeadless"
"test": "vitest run --browser",
"test:watch": "vitest --browser",
"test-benchmark": "vitest run --browser",
"test-benchmark:watch": "vitest --browser"
},
"keywords": [
"HTML5",
@@ -30,12 +24,9 @@
"mocha": "^1.21.4"
},
"devDependencies": {
"@chiragrupani/karma-chromium-edge-launcher": "2.1.1",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.3.0",
"karma-sinon": "^1.0.5",
"vitest": "^0.34.0",
"vite": "^4.4.9",
"@vitest/browser": "^0.34.0",
"sinon": "^15.0.1"
}
}

View File

@@ -16,6 +16,7 @@ describe('gdjs.CustomRuntimeObject', function () {
name: 'MyCustomObject',
type: 'MyExtension::MyEventsBasedObject',
variant: '',
isInnerAreaFollowingParentSize: false,
variables: [],
behaviors: [],
effects: [],

View File

@@ -27,7 +27,7 @@
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
const asyncTasksManager = runtimeScene.getAsyncTasksManager();
this.beforeEach(() => asyncTasksManager.clearTasks());
beforeEach(() => asyncTasksManager.clearTasks());
it('should call a resolved callback', function () {
const cb = createMockCallback();

View File

@@ -100,6 +100,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
effects: [],
content: {},
childrenContent: {},
isInnerAreaFollowingParentSize: false,
};
/** @type {LayerData} */

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chrome',
headless: true,
},
setupFiles: [
'./vitest.setup.js',
'./tests-utils/init.js',
'./tests-utils/init.pixiruntimegamewithassets.js',
'./tests-utils/init.pixiruntimegame.js',
'./tests-utils/MockedCustomObject.js',
],
include: ['tests/**/*.js', 'benchmarks/**/*.js', '../Extensions/**/tests/**/*.js'],
},
});

View File

@@ -0,0 +1,9 @@
import expect from 'expect.js';
import sinon from 'sinon';
// Expose expect.js and sinon globally like Karma used to do.
// Vitest provides its own expect, but tests rely on expect.js syntax.
// eslint-disable-next-line no-undef
global.expect = expect;
// eslint-disable-next-line no-undef
global.sinon = sinon;

View File

@@ -1405,6 +1405,7 @@ interface InitialInstance {
double GetCustomDepth();
[Ref] InitialInstance ResetPersistentUuid();
[Const, Ref] DOMString GetPersistentUuid();
void UpdateCustomProperty(
[Const] DOMString name,

View File

@@ -1167,6 +1167,7 @@ export class InitialInstance extends EmscriptenObject {
setCustomDepth(depth: number): void;
getCustomDepth(): number;
resetPersistentUuid(): InitialInstance;
getPersistentUuid(): string;
updateCustomProperty(name: string, value: string, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): void;
getCustomProperties(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): MapStringPropertyDescriptor;
getRawDoubleProperty(name: string): number;

View File

@@ -44,6 +44,7 @@ declare class gdInitialInstance {
setCustomDepth(depth: number): void;
getCustomDepth(): number;
resetPersistentUuid(): gdInitialInstance;
getPersistentUuid(): string;
updateCustomProperty(name: string, value: string, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): void;
getCustomProperties(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): gdMapStringPropertyDescriptor;
getRawDoubleProperty(name: string): number;

View File

@@ -30,6 +30,21 @@
justify-content: center;
animation: new-chat-appear 0.5s;
margin-bottom: var(--safe-area-inset-bottom);
}
.aiRequestChatContainer {
display: flex;
margin-left: 8px;
margin-right: 8px;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
flex: 1 1 0%;
min-height: 0px;
margin-bottom: var(--safe-area-inset-bottom);
}
.thinkingText {

View File

@@ -11,7 +11,7 @@ import { Tooltip } from '@material-ui/core';
import Text from '../../UI/Text';
import RaisedButton from '../../UI/RaisedButton';
import { Trans } from '@lingui/macro';
import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu';
import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu';
import Check from '../../UI/CustomSvgIcons/Check';
import Error from '../../UI/CustomSvgIcons/Error';
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
@@ -22,7 +22,7 @@ import {
type EditorCallbacks,
} from '../../EditorFunctions';
import Link from '../../UI/Link';
import { LineStackLayout } from '../../UI/Layout';
import { LineStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom';
import Paper from '../../UI/Paper';
@@ -108,6 +108,7 @@ export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
details = result.details;
hasDetailsToShow = result.hasDetailsToShow;
} catch (error) {
console.error('Error rendering function call:', error);
text = (
<Trans>
The editor was unable to display the operation ({functionCall.name})
@@ -141,60 +142,73 @@ export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
)}
</div>
</Tooltip>
<LineStackLayout noMargin alignItems="baseline">
<Text>{text || 'Working...'}</Text>
{hasDetailsToShow && (
<Text size="body-small" color="secondary">
<Link
color="inherit"
href={'#'}
onClick={() => setShowDetails(!showDetails)}
>
<Trans>Details</Trans>
{details ? (
<ChevronArrowBottom
fontSize="small"
style={{
verticalAlign: 'middle',
}}
/>
) : (
<ChevronArrowRight
fontSize="small"
style={{
verticalAlign: 'middle',
}}
/>
)}
</Link>
</Text>
)}
</LineStackLayout>
{!isFinished && (
<RaisedButtonWithSplitMenu
primary
disabled={isWorking}
onClick={() => onProcessFunctionCalls([functionCall])}
label={<Trans>Apply</Trans>}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Ignore this`),
click: () => {
onProcessFunctionCalls([functionCall], {
ignore: true,
});
},
},
]}
/>
)}
{functionCallResultIsErrored && (
<RaisedButton
color="primary"
onClick={() => onProcessFunctionCalls([functionCall])}
label={<Trans>Retry</Trans>}
/>
)}
<ResponsiveLineStackLayout
justifyContent="space-between"
expand
noOverflowParent
>
<LineStackLayout noMargin alignItems="baseline">
<Text>{text || 'Working...'}</Text>
{hasDetailsToShow && (
<Text size="body-small" color="secondary">
<Link
color="inherit"
href={'#'}
onClick={() => setShowDetails(!showDetails)}
>
<Trans>Details</Trans>
{details ? (
<ChevronArrowBottom
fontSize="small"
style={{
verticalAlign: 'middle',
}}
/>
) : (
<ChevronArrowRight
fontSize="small"
style={{
verticalAlign: 'middle',
}}
/>
)}
</Link>
</Text>
)}
</LineStackLayout>
<LineStackLayout
noMargin
alignItems="baseline"
justifyContent="flex-end"
neverShrink
>
{!isFinished && !isWorking && (
<FlatButtonWithSplitMenu
primary
style={{ flexShrink: 0 }}
onClick={() => onProcessFunctionCalls([functionCall])}
label={<Trans>Execute this action</Trans>}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Ignore this`),
click: () => {
onProcessFunctionCalls([functionCall], {
ignore: true,
});
},
},
]}
/>
)}
{functionCallResultIsErrored && (
<RaisedButton
color="primary"
onClick={() => onProcessFunctionCalls([functionCall])}
label={<Trans>Retry</Trans>}
/>
)}
</LineStackLayout>
</ResponsiveLineStackLayout>
</LineStackLayout>
{details && (
<div className={classes.detailsPaperContainer}>

View File

@@ -37,7 +37,7 @@ import Hammer from '../../UI/CustomSvgIcons/Hammer';
import { ChatMessages } from './ChatMessages';
import Send from '../../UI/CustomSvgIcons/Send';
import { FeedbackBanner } from './FeedbackBanner';
import LeftLoader from '../../UI/LeftLoader';
import classNames from 'classnames';
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 5;
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 10;
@@ -79,6 +79,7 @@ type Props = {
hasOpenedProject: boolean,
isAutoProcessingFunctionCalls: boolean,
setAutoProcessFunctionCalls: boolean => void,
onStartNewChat: () => void,
onProcessFunctionCalls: (
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
@@ -232,6 +233,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
onStartNewAiRequest,
onSendMessage,
onSendFeedback,
onStartNewChat,
quota,
increaseQuotaOffering,
lastSendError,
@@ -257,6 +259,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
setUserRequestTextPerRequestId,
] = React.useState<{ [string]: string }>({});
const scrollViewRef = React.useRef<ScrollViewInterface | null>(null);
const requiredGameId = (aiRequest && aiRequest.gameId) || null;
const newChatPlaceholder = React.useMemo(
() => {
@@ -266,7 +269,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
? [
t`Add solid rocks that falls from the sky at a random position around the player every 0.5 seconds`,
t`Add a score and display it on the screen`,
t`Create an explosion when the player is hit`,
t`Create a 3D explosion when the player is hit`,
]
: [
t`Build a platformer game with a score and coins to collect`,
@@ -328,6 +331,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
const subscriptionBanner =
quota && quota.limitReached && increaseQuotaOffering !== 'none' ? (
<GetSubscriptionCard
placementId="ai-requests"
subscriptionDialogOpeningReason={
increaseQuotaOffering === 'subscribe'
? 'AI requests (subscribe)'
@@ -384,7 +388,13 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
if (!aiRequest) {
return (
<div className={classes.newChatContainer}>
<div
className={classNames({
[classes.newChatContainer]: true,
// Move the entire screen up when the soft keyboard is open:
'avoid-soft-keyboard': true,
})}
>
<ColumnStackLayout justifyContent="center" expand>
<Line noMargin justifyContent="center">
<RobotIcon rotating size={40} />
@@ -434,6 +444,8 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
maxLength={6000}
value={userRequestTextPerAiRequestId[''] || ''}
disabled={isSending}
hasNeonCorner
hasAnimatedNeonCorner={isSending}
errored={!!lastSendError}
onChange={userRequestText =>
setUserRequestTextPerRequestId(
@@ -457,35 +469,32 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
alignItems="center"
justifyContent="flex-end"
>
<LeftLoader isLoading={isSending}>
<RaisedButton
color="primary"
icon={<Send />}
label={
newAiRequestMode === 'agent' ? (
hasOpenedProject ? (
<Trans>Build this on my game</Trans>
) : (
<Trans>Start building the game</Trans>
)
<RaisedButton
color="primary"
icon={<Send />}
label={
newAiRequestMode === 'agent' ? (
hasOpenedProject ? (
<Trans>Build this on my game</Trans>
) : (
<Trans>Send question</Trans>
<Trans>Start building the game</Trans>
)
}
style={{ flexShrink: 0 }}
disabled={
isSending ||
!userRequestTextPerAiRequestId[aiRequestId]
}
onClick={() => {
onStartNewAiRequest({
mode: newAiRequestMode,
userRequest:
userRequestTextPerAiRequestId[''],
});
}}
/>
</LeftLoader>
) : (
<Trans>Send question</Trans>
)
}
style={{ flexShrink: 0 }}
disabled={
isSending ||
!userRequestTextPerAiRequestId[aiRequestId]
}
onClick={() => {
onStartNewAiRequest({
mode: newAiRequestMode,
userRequest: userRequestTextPerAiRequestId[''],
});
}}
/>
</LineStackLayout>
</Column>
}
@@ -517,10 +526,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<Trans>
The AI agent will build simple games or features for you.{' '}
<Link
href={getHelpLink('/interface/ask-ai')}
href={getHelpLink('/interface/ai')}
color="secondary"
onClick={() =>
Window.openExternalURL(getHelpLink('/interface/ask-ai'))
Window.openExternalURL(getHelpLink('/interface/ai'))
}
>
It can inspect your game objects and events.
@@ -532,10 +541,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<Trans>
The AI chat is experimental and still being improved.{' '}
<Link
href={getHelpLink('/interface/ask-ai')}
href={getHelpLink('/interface/ai')}
color="secondary"
onClick={() =>
Window.openExternalURL(getHelpLink('/interface/ask-ai'))
Window.openExternalURL(getHelpLink('/interface/ai'))
}
>
It has access to your game objects but not events.
@@ -572,11 +581,14 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
aiRequest,
editorFunctionCallResults,
});
const isPausedAndHasFunctionCallsToProcess =
!isAutoProcessingFunctionCalls && allFunctionCallsToProcess.length > 0;
const lastMessageIndex = aiRequest.output.length - 1;
const lastMessage = aiRequest.output[lastMessageIndex];
const shouldDisplayFeedbackBanner =
!hasWorkingFunctionCalls &&
!isPausedAndHasFunctionCallsToProcess &&
!isSending &&
aiRequest.status === 'ready' &&
aiRequest.mode === 'agent' &&
@@ -600,12 +612,26 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
/>
);
const isForAnotherProject =
!!requiredGameId &&
(!project || requiredGameId !== project.getProjectUuid());
const isForAnotherProjectText = isForAnotherProject ? (
<Text size="body-small" color="secondary" align="center" noMargin>
<Trans>
This request is for another project.{' '}
<Link href="#" onClick={onStartNewChat}>
Start a new chat
</Link>{' '}
to build on a new project.
</Trans>
</Text>
) : null;
return (
<Column
expand
alignItems="stretch"
justifyContent="stretch"
useFullHeight
<div
className={classNames({
[classes.aiRequestChatContainer]: true,
})}
>
<ScrollView ref={scrollViewRef} style={styles.chatScrollView}>
<ChatMessages
@@ -644,24 +670,29 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
userMessage: userRequestTextPerAiRequestId[aiRequestId] || '',
});
}}
className={classNames({
// Move the form up when the soft keyboard is open:
'avoid-soft-keyboard': true,
})}
>
<ColumnStackLayout
justifyContent="stretch"
alignItems="stretch"
noMargin
>
{isAutoProcessingFunctionCalls &&
{aiRequest.mode === 'agent' &&
isAutoProcessingFunctionCalls &&
(hasWorkingFunctionCalls ||
isSending ||
aiRequest.status === 'working') ? (
<Paper background="dark" variant="outlined" square>
<Paper background="dark" variant="outlined">
<Column>
<LineStackLayout
justifyContent="space-between"
alignItems="center"
>
<LineStackLayout alignItems="center" noMargin>
<CircularProgress variant="indeterminate" size={10} />
<CircularProgress variant="indeterminate" size={12} />
<Text size="body" color="secondary" noMargin>
<Trans>The AI is building your request.</Trans>
</Text>
@@ -680,9 +711,9 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
</LineStackLayout>
</Column>
</Paper>
) : !isAutoProcessingFunctionCalls &&
allFunctionCallsToProcess.length > 0 ? (
<Paper background="dark" variant="outlined" square>
) : aiRequest.mode === 'agent' &&
isPausedAndHasFunctionCallsToProcess ? (
<Paper background="dark" variant="outlined">
<Column>
<LineStackLayout
justifyContent="space-between"
@@ -702,9 +733,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
onProcessFunctionCalls(allFunctionCallsToProcess);
}}
>
<Trans>
Apply everything and continue autonomously
</Trans>
<Trans>Resume all</Trans>
</Link>
</Text>
</LineStackLayout>
@@ -714,8 +743,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<CompactTextAreaFieldWithControls
maxLength={6000}
value={userRequestTextPerAiRequestId[aiRequestId] || ''}
disabled={isSending}
disabled={isSending || isForAnotherProject}
errored={!!lastSendError}
hasNeonCorner
hasAnimatedNeonCorner={isSending}
onChange={userRequestText =>
setUserRequestTextPerRequestId(
userRequestTextPerAiRequestId => ({
@@ -726,7 +757,9 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
}
placeholder={
aiRequest.mode === 'agent'
? t`Specify something more to the AI to build`
? isForAnotherProject
? t`You must re-open the project to continue this chat.`
: t`Specify something more to the AI to build`
: t`Ask a follow up question`
}
rows={2}
@@ -746,6 +779,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
disabled={
aiRequest.status === 'working' ||
isSending ||
isForAnotherProject ||
!userRequestTextPerAiRequestId[aiRequestId]
}
icon={<Send />}
@@ -767,13 +801,15 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
alignItems="center"
justifyContent="space-between"
>
{errorText || priceText}
{errorText ? null : quotaOrCreditsText}
{isForAnotherProjectText || errorText || priceText}
{errorText || isForAnotherProjectText
? null
: quotaOrCreditsText}
</LineStackLayout>
</Column>
</ColumnStackLayout>
</form>
</Column>
</div>
);
}
);

View File

@@ -112,15 +112,18 @@ const useProcessFunctionCalls = ({
i18n,
project,
resourceManagementProps,
editorCallbacks,
selectedAiRequest,
onSendEditorFunctionCallResults,
getEditorFunctionCallResults,
addEditorFunctionCallResults,
onSceneEventsModifiedOutsideEditor,
onExtensionInstalled,
}: {|
i18n: I18nType,
project: gdProject | null,
resourceManagementProps: ResourceManagementProps,
editorCallbacks: EditorCallbacks,
selectedAiRequest: ?AiRequest,
onSendEditorFunctionCallResults: () => Promise<void>,
getEditorFunctionCallResults: string => Array<EditorFunctionCallResult> | null,
@@ -129,6 +132,7 @@ const useProcessFunctionCalls = ({
Array<EditorFunctionCallResult>
) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|}) => {
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
project,
@@ -137,6 +141,7 @@ const useProcessFunctionCalls = ({
const { searchAndInstallAsset } = useSearchAndInstallAsset({
project,
resourceManagementProps,
onExtensionInstalled,
});
const { generateEvents } = useGenerateEvents({ project });
@@ -187,6 +192,7 @@ const useProcessFunctionCalls = ({
const editorFunctionCallResults = await processEditorFunctionCalls({
project,
editorCallbacks,
functionCalls: functionCalls.map(functionCall => ({
name: functionCall.name,
arguments: functionCall.arguments,
@@ -222,6 +228,7 @@ const useProcessFunctionCalls = ({
generateEvents,
onSceneEventsModifiedOutsideEditor,
triggerSendEditorFunctionCallResults,
editorCallbacks,
]
);
@@ -454,9 +461,20 @@ type Props = {|
i18n: I18nType,
isQuickCustomization?: boolean
) => Promise<void>,
onOpenLayout: (sceneName: string) => void,
onOpenEvents: (sceneName: string) => void,
onOpenLayout: (
sceneName: string,
options: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|}
) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
export type AskAiEditorInterface = {|
@@ -470,6 +488,7 @@ export type AskAiEditorInterface = {|
) => void,
onSceneObjectsDeleted: (scene: gdLayout) => void,
onSceneEventsModifiedOutsideEditor: (scene: gdLayout) => void,
startNewChat: () => void,
|};
export type NewAiRequestOptions = {|
@@ -493,17 +512,16 @@ export const AskAiEditor = React.memo<Props>(
onCreateEmptyProject,
onCreateProjectFromExample,
onOpenLayout,
onOpenEvents,
onSceneEventsModifiedOutsideEditor,
onExtensionInstalled,
}: Props,
ref
) => {
const editorCallbacks: EditorCallbacks = React.useMemo(
() => ({
onOpenLayout,
onOpenEvents,
}),
[onOpenLayout, onOpenEvents]
[onOpenLayout]
);
const {
@@ -580,6 +598,7 @@ export const AskAiEditor = React.memo<Props>(
onSceneObjectEdited: noop,
onSceneObjectsDeleted: noop,
onSceneEventsModifiedOutsideEditor: noop,
startNewChat: onStartNewChat,
}));
const aiRequestChatRef = React.useRef<AiRequestChatInterface | null>(
@@ -770,6 +789,19 @@ export const AskAiEditor = React.memo<Props>(
]
);
const hasFunctionsCallsToProcess = React.useMemo(
() =>
selectedAiRequest
? getFunctionCallsToProcess({
aiRequest: selectedAiRequest,
editorFunctionCallResults: getEditorFunctionCallResults(
selectedAiRequest.id
),
}).length > 0
: false,
[selectedAiRequest, getEditorFunctionCallResults]
);
// Send the results of the function call outputs, if any, and the user message (if any).
const onSendMessage = React.useCallback(
async ({ userMessage }: {| userMessage: string |}) => {
@@ -792,6 +824,7 @@ export const AskAiEditor = React.memo<Props>(
// If anything is not finished yet, stop there (we only send all
// results at once, AI do not support partial results).
if (hasUnfinishedResult) return;
if (hasFunctionsCallsToProcess) return;
// If nothing to send, stop there.
if (functionCallOutputs.length === 0 && !userMessage) return;
@@ -894,6 +927,7 @@ export const AskAiEditor = React.memo<Props>(
setLastSendError,
onRefreshLimits,
project,
hasFunctionsCallsToProcess,
]
);
const onSendEditorFunctionCallResults = React.useCallback(
@@ -939,11 +973,13 @@ export const AskAiEditor = React.memo<Props>(
project,
resourceManagementProps,
selectedAiRequest,
editorCallbacks,
onSendEditorFunctionCallResults,
getEditorFunctionCallResults,
addEditorFunctionCallResults,
onSceneEventsModifiedOutsideEditor,
i18n,
onExtensionInstalled,
});
return (
@@ -990,6 +1026,7 @@ export const AskAiEditor = React.memo<Props>(
}}
i18n={i18n}
editorCallbacks={editorCallbacks}
onStartNewChat={onStartNewChat}
/>
</div>
</Paper>
@@ -1034,10 +1071,10 @@ export const renderAskAiEditorContainer = (
onCreateEmptyProject={props.onCreateEmptyProject}
onCreateProjectFromExample={props.onCreateProjectFromExample}
onOpenLayout={props.onOpenLayout}
onOpenEvents={props.onOpenEvents}
onSceneEventsModifiedOutsideEditor={
props.onSceneEventsModifiedOutsideEditor
}
onExtensionInstalled={props.onExtensionInstalled}
/>
)}
</I18n>

View File

@@ -12,6 +12,7 @@ import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
import { generateProjectName } from '../ProjectCreation/NewProjectSetupDialog';
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
import { Spacer } from '../UI/Grid';
type RenderCreateAiProjectDialogProps = {
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
@@ -50,12 +51,10 @@ const CreateAiProjectDialog = ({
/>,
]}
onRequestClose={onClose}
onApply={() => {
// TODO
}}
flexColumnBody
>
<ColumnStackLayout noMargin>
<Spacer />
<EmptyAndStartingPointProjects
onSelectExampleShortHeader={exampleShortHeader => {
onSelectExampleShortHeader(exampleShortHeader);
@@ -64,6 +63,8 @@ const CreateAiProjectDialog = ({
onSelectEmptyProject();
}}
/>
{/* Use a spacer to avoid extra scrollbars when template tiles are hovered. */}
<Spacer />
</ColumnStackLayout>
</Dialog>
);

View File

@@ -33,7 +33,7 @@ export const useEnsureExtensionInstalled = ({
const extensionShortHeader =
translatedExtensionShortHeadersByName[extensionName];
if (!extensionShortHeader) {
throw new Error("Can't find extension with the required name.");
throw new Error(`Can't find extension named ${extensionName}.`);
}
await installExtension(

View File

@@ -16,9 +16,11 @@ import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
export const useSearchAndInstallAsset = ({
project,
resourceManagementProps,
onExtensionInstalled,
}: {|
project: gdProject | null,
resourceManagementProps: ResourceManagementProps,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|}) => {
const { profile, getAuthorizationHeader } = React.useContext(
AuthenticatedUserContext
@@ -26,6 +28,7 @@ export const useSearchAndInstallAsset = ({
const installAsset = useInstallAsset({
project,
resourceManagementProps,
onExtensionInstalled,
});
return {

View File

@@ -49,6 +49,7 @@ type Props = {|
addedAssetIds: Set<string>,
onClose: () => void,
onAssetsAdded: (createdObjects: gdObject[]) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
project: gdProject,
objectsContainer: ?gdObjectsContainer,
resourceManagementProps: ResourceManagementProps,
@@ -61,6 +62,7 @@ const AssetPackInstallDialog = ({
addedAssetIds,
onClose,
onAssetsAdded,
onExtensionInstalled,
project,
objectsContainer,
resourceManagementProps,
@@ -176,6 +178,7 @@ const AssetPackInstallDialog = ({
shouldUpdateExtension: extensionUpdateAction === 'update',
eventsFunctionsExtensionsState,
project,
onExtensionInstalled,
});
// Use a pool to avoid installing an unbounded amount of assets at the same time.
@@ -253,6 +256,7 @@ const AssetPackInstallDialog = ({
eventsFunctionsExtensionsState,
resourceManagementProps,
onAssetsAdded,
onExtensionInstalled,
installPrivateAsset,
targetObjectsContainer,
targetObjectFolderOrObjectWithContext,

View File

@@ -29,6 +29,7 @@ type Props = {|
onClose: ({ swappingDone: boolean }) => void,
// Use minimal UI to hide filters & the details page (useful for Quick Customization)
minimalUI?: boolean,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
function AssetSwappingDialog({
@@ -40,6 +41,7 @@ function AssetSwappingDialog({
resourceManagementProps,
onClose,
minimalUI,
onExtensionInstalled,
}: Props) {
const shopNavigationState = React.useContext(AssetStoreNavigatorContext);
const { openedAssetShortHeader } = shopNavigationState.getCurrentPage();
@@ -51,6 +53,7 @@ function AssetSwappingDialog({
const installAsset = useInstallAsset({
project,
resourceManagementProps,
onExtensionInstalled,
});
const { showAlert } = useAlertDialog();

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

@@ -25,7 +25,7 @@ type Props = {|
project: gdProject,
onClose: () => void,
onInstallExtension: (extensionName: string) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
onCreateNew?: () => void,
|};
@@ -84,7 +84,7 @@ const ExtensionsSearchDialog = ({
if (installedOrImportedExtensionName) {
setExtensionWasInstalled(true);
onExtensionInstalled(installedOrImportedExtensionName);
onExtensionInstalled([installedOrImportedExtensionName]);
return true;
}

View File

@@ -381,6 +381,7 @@ export type InstallRequiredExtensionsArgs = {|
shouldUpdateExtension: boolean,
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
project: gdProject,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
export const installRequiredExtensions = async ({
@@ -388,6 +389,7 @@ export const installRequiredExtensions = async ({
shouldUpdateExtension,
eventsFunctionsExtensionsState,
project,
onExtensionInstalled,
}: InstallRequiredExtensionsArgs): Promise<void> => {
const {
requiredExtensionShortHeaders,
@@ -417,6 +419,9 @@ export const installRequiredExtensions = async ({
project,
serializedExtensions
);
onExtensionInstalled(
neededExtensions.map(extensionShortHeader => extensionShortHeader.name)
);
const stillMissingExtensions = filterMissingExtensions(
gd,

View File

@@ -1034,6 +1034,7 @@ describe('InstallAsset', () => {
shouldUpdateExtension: true,
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
project,
onExtensionInstalled: () => {},
})
).rejects.toMatchObject({
// It's just because the mock doesn't reloadProjectEventsFunctionsExtensions.
@@ -1069,6 +1070,7 @@ describe('InstallAsset', () => {
shouldUpdateExtension: true,
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
project,
onExtensionInstalled: () => {},
})
).rejects.toMatchObject({
message: 'These extensions could not be installed: Flash',
@@ -1109,6 +1111,7 @@ describe('InstallAsset', () => {
shouldUpdateExtension: true,
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
project,
onExtensionInstalled: () => {},
});
// No extensions fetched because the extension is already installed.

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>
@@ -174,10 +181,12 @@ export const useInstallAsset = ({
project,
targetObjectFolderOrObjectWithContext,
resourceManagementProps,
onExtensionInstalled,
}: {|
project: gdProject | null,
targetObjectFolderOrObjectWithContext?: ?ObjectFolderOrObjectWithContext,
resourceManagementProps: ResourceManagementProps,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|}) => {
const shopNavigationState = React.useContext(AssetStoreNavigatorContext);
const { openedAssetPack } = shopNavigationState.getCurrentPage();
@@ -242,6 +251,7 @@ export const useInstallAsset = ({
shouldUpdateExtension: extensionUpdateAction === 'update',
eventsFunctionsExtensionsState,
project,
onExtensionInstalled,
});
const isPrivate = isPrivateAsset(assetShortHeader);
const installOutput = isPrivate
@@ -309,6 +319,7 @@ type Props = {|
onCreateNewObject: (type: string) => void,
onObjectsAddedFromAssets: (Array<gdObject>) => void,
targetObjectFolderOrObjectWithContext?: ?ObjectFolderOrObjectWithContext,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
function NewObjectDialog({
@@ -321,6 +332,7 @@ function NewObjectDialog({
onCreateNewObject,
onObjectsAddedFromAssets,
targetObjectFolderOrObjectWithContext,
onExtensionInstalled,
}: Props) {
const { isMobile } = useResponsiveWindowSize();
const {
@@ -378,6 +390,7 @@ function NewObjectDialog({
project,
resourceManagementProps,
targetObjectFolderOrObjectWithContext,
onExtensionInstalled,
});
const onInstallAsset = React.useCallback(
@@ -436,6 +449,7 @@ function NewObjectDialog({
shouldUpdateExtension: extensionUpdateAction === 'update',
eventsFunctionsExtensionsState,
project,
onExtensionInstalled,
});
onCreateNewObject(enumeratedObjectMetadata.name);
@@ -457,6 +471,7 @@ function NewObjectDialog({
showExtensionUpdateConfirmation,
eventsFunctionsExtensionsState,
showAlert,
onExtensionInstalled,
]
);
@@ -671,6 +686,7 @@ function NewObjectDialog({
targetObjectFolderOrObjectWithContext={
targetObjectFolderOrObjectWithContext
}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</>

View File

@@ -656,6 +656,7 @@ const PrivateAssetPackInformationPage = ({
analyticsMetadata: {
reason: 'Claim asset pack',
recommendedPlanId: 'gdevelop_gold',
placementId: 'claim-asset-pack',
},
filter: 'individual',
})

View File

@@ -32,7 +32,7 @@ type Props = {|
open: boolean,
onClose: () => void,
onChoose: (type: string, defaultName: string) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
export default function NewBehaviorDialog({
@@ -190,7 +190,7 @@ export default function NewBehaviorDialog({
behaviorShortHeader
);
if (wasExtensionInstalled) {
onExtensionInstalled(behaviorShortHeader.extensionName);
onExtensionInstalled([behaviorShortHeader.extensionName]);
}
return wasExtensionInstalled;
} finally {

View File

@@ -325,7 +325,7 @@ export const useManageObjectBehaviors = ({
onSizeUpdated?: ?() => void,
onBehaviorsUpdated?: ?() => void,
onUpdateBehaviorsSharedData: () => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
}): UseManageBehaviorsState => {
const [
justAddedBehaviorName,
@@ -623,7 +623,7 @@ type Props = {|
extensionName: string,
behaviorName: string
) => Promise<void>,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
isListLocked: boolean,
|};

View File

@@ -174,6 +174,7 @@ const LockedCourseChapterPreview = React.forwardRef<Props, HTMLDivElement>(
analyticsMetadata: {
reason: 'Unlock course chapter',
recommendedPlanId: 'gdevelop_silver',
placementId: 'unlock-course-chapter',
},
})
}

View File

@@ -3,6 +3,7 @@ import { type EventsGenerationResult } from '.';
import {
editorFunctions,
type EditorFunction,
type EditorCallbacks,
type EditorFunctionCall,
type EditorFunctionGenericOutput,
type EventsGenerationOptions,
@@ -29,6 +30,7 @@ export type EditorFunctionCallResult =
export type ProcessEditorFunctionCallsOptions = {|
project: gdProject,
functionCalls: Array<EditorFunctionCall>,
editorCallbacks: EditorCallbacks,
ignore: boolean,
generateEvents: (
options: EventsGenerationOptions
@@ -45,6 +47,7 @@ export type ProcessEditorFunctionCallsOptions = {|
export const processEditorFunctionCalls = async ({
functionCalls,
project,
editorCallbacks,
generateEvents,
onSceneEventsModifiedOutsideEditor,
ignore,
@@ -139,6 +142,16 @@ export const processEditorFunctionCalls = async ({
success,
output,
});
if (success && args) {
if (typeof args.scene_name === 'string') {
editorCallbacks.onOpenLayout(args.scene_name, {
openEventsEditor: true,
openSceneEditor: true,
focusWhenOpened: 'none',
});
}
}
} catch (error) {
results.push({
status: 'finished',

View File

@@ -23,6 +23,7 @@ type SimplifiedObject = {|
objectType: string,
behaviors?: Array<SimplifiedBehavior>,
objectVariables?: Array<SimplifiedVariable>,
animationNames?: string,
|};
type SimplifiedObjectGroup = {|
@@ -165,6 +166,21 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
simplifiedObject.objectVariables = objectVariables;
}
const objectConfiguration = object.getConfiguration();
const animationNames = mapFor(
0,
objectConfiguration.getAnimationsCount(),
i => {
return (
objectConfiguration.getAnimationName(i) ||
`(animation without name, animation index is: ${i})`
);
}
);
if (animationNames.length > 0) {
simplifiedObject.animationNames = animationNames.join(', ');
}
return simplifiedObject;
};

View File

@@ -400,6 +400,7 @@ describe('SimplifiedProject', () => {
"objectType": "Sprite",
},
Object {
"animationNames": "My animation, My other animation, (animation without name, animation index is: 2)",
"behaviors": Array [
Object {
"behaviorName": "Animation",

File diff suppressed because it is too large Load Diff

View File

@@ -89,7 +89,7 @@ type Props = {|
eventsFunctionsExtension: gdEventsFunctionsExtension,
name: string
) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
type State = {|

View File

@@ -71,7 +71,7 @@ type Props = {|
anchorEl?: any, // Unused
canPasteInstructions: boolean, // Unused
onPasteInstructions: () => void, // Unused
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
const getInitialStepName = (isNewInstruction: boolean): StepName => {

View File

@@ -56,7 +56,7 @@ type Props = {|
i18n: I18nType,
canPasteInstructions: boolean, // Unused
onPasteInstructions: () => void, // Unused
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
/**

View File

@@ -154,7 +154,7 @@ type Props = {|
unsavedChanges?: ?UnsavedChanges,
isActive: boolean,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
type ComponentProps = {|

View File

@@ -371,6 +371,7 @@ export default class LocalPreviewLauncher extends React.Component<
}
id="Preview over wifi"
title={<Trans>Preview over wifi</Trans>}
placementId="preview-wifi"
mode="try"
isNotShownDuringInAppTutorial
/>
@@ -382,6 +383,7 @@ export default class LocalPreviewLauncher extends React.Component<
title={
<Trans>Live preview (apply changes to the running preview)</Trans>
}
placementId="hot-reloading"
mode="try"
isNotShownDuringInAppTutorial
/>

View File

@@ -326,6 +326,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
<GetSubscriptionCard
subscriptionDialogOpeningReason="Add collaborators on project"
recommendedPlanIdIfNoSubscription="gdevelop_startup"
placementId="invite-collaborators"
>
<Text>
<Trans>

View File

@@ -350,6 +350,7 @@ function LeaderboardAppearanceDialog({
<GetSubscriptionCard
subscriptionDialogOpeningReason="Leaderboard customization"
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="leaderboards-customization"
>
<Line>
<Column noMargin>
@@ -406,6 +407,7 @@ function LeaderboardAppearanceDialog({
<GetSubscriptionCard
subscriptionDialogOpeningReason="Leaderboard customization"
recommendedPlanIdIfNoSubscription="gdevelop_startup"
placementId="leaderboards-customization"
>
<Line>
<Column noMargin>

View File

@@ -256,6 +256,7 @@ function LeaderboardOptionsDialog({
<GetSubscriptionCard
subscriptionDialogOpeningReason="Leaderboard customization"
recommendedPlanIdIfNoSubscription="gdevelop_startup"
placementId="leaderboards-customization"
>
<Line>
<Column noMargin>

View File

@@ -47,6 +47,7 @@ const MaxLeaderboardCountAlertMessage = () => {
<Column expand>
<GetSubscriptionCard
subscriptionDialogOpeningReason="Leaderboard count per game limit reached"
placementId="leaderboards"
label={
!hasValidSubscription ? (
<Trans>Upgrade to GDevelop Premium</Trans>

View File

@@ -98,6 +98,7 @@ const ServicesWidget = ({
analyticsMetadata: {
reason: 'Leaderboard count per game limit reached',
recommendedPlanId: 'gdevelop_silver',
placementId: 'leaderboards',
},
})
}

View File

@@ -55,7 +55,18 @@ export type RenderEditorContainerProps = {|
// Opening other editors:
onOpenExternalEvents: string => void,
onOpenLayout: string => void,
onOpenLayout: (
sceneName: string,
options?: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|}
) => void,
onOpenEvents: (sceneName: string) => void,
openInstructionOrExpression: (
extension: gdPlatformExtension,
@@ -171,7 +182,7 @@ export type RenderEditorContainerProps = {|
eventsBasedObjectName: string,
variantName: string
) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
onDeleteEventsBasedObjectVariant: (
eventsFunctionsExtension: gdEventsFunctionsExtension,
eventBasedObject: gdEventsBasedObject,

View File

@@ -105,6 +105,7 @@ export class DebuggerEditorContainer extends React.Component<
}
id="Debugger"
title={<Trans>Debugger</Trans>}
placementId="debugger"
mode="try"
/>
</React.Fragment>

View File

@@ -55,6 +55,7 @@ export const MaxProjectCountAlertMessage = ({ margin }: Props) => {
}
hideButton={!canMaximumCountBeIncreased}
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="max-projects-reached"
>
<Line>
<Column noMargin expand>

View File

@@ -241,6 +241,7 @@ const EducationMarketingSection = ({
analyticsMetadata: {
reason: 'Callout in Classroom tab',
recommendedPlanId: 'gdevelop_education',
placementId: 'education',
},
});
},

View File

@@ -21,6 +21,7 @@ type Props = {|
privateGameTemplateListingData: PrivateGameTemplateListingData
) => void,
onOpenProfile: () => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
const StoreSection = ({
@@ -28,6 +29,7 @@ const StoreSection = ({
resourceManagementProps,
onOpenPrivateGameTemplateListingData,
onOpenProfile,
onExtensionInstalled,
}: Props) => {
const [
isAssetPackDialogInstallOpen,
@@ -129,6 +131,7 @@ const StoreSection = ({
project={project}
objectsContainer={null}
resourceManagementProps={resourceManagementProps}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</SectionContainer>

View File

@@ -638,6 +638,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
analyticsMetadata: {
reason: 'Manage subscription as teacher',
recommendedPlanId: 'gdevelop_education',
placementId: 'education',
},
filter: 'education',
})

View File

@@ -152,6 +152,9 @@ type Props = {|
templateId?: string
) => Promise<void>,
// Asset store
onExtensionInstalled: (extensionNames: Array<string>) => void,
// Project save
onSave: () => Promise<void>,
canSave: boolean,
@@ -205,6 +208,7 @@ export const HomePage = React.memo<Props>(
onOpenTemplateFromCourseChapter,
gamesList,
gamesPlatformFrameTools,
onExtensionInstalled,
}: Props,
ref
) => {
@@ -648,6 +652,7 @@ export const HomePage = React.memo<Props>(
onOpenPrivateGameTemplateListingData
}
onOpenProfile={onOpenProfile}
onExtensionInstalled={onExtensionInstalled}
/>
)}
{activeTab === 'team-view' &&
@@ -733,5 +738,6 @@ export const renderHomePageContainer = (
resourceManagementProps={props.resourceManagementProps}
gamesList={props.gamesList}
gamesPlatformFrameTools={props.gamesPlatformFrameTools}
onExtensionInstalled={props.onExtensionInstalled}
/>
);

View File

@@ -485,3 +485,12 @@ export const hasEditorTabOpenedWithKey = (
) => {
return !!editorTabsState.editors.find(editor => editor.key === key);
};
export const getOpenedAskAiEditor = (
state: EditorTabsState
): AskAiEditorInterface | null => {
const editor = state.editors.find(editor => editor.key === 'ask-ai');
// $FlowFixMe - the key ensures that the editor is an AskAiEditorInterface.
return (editor && editor.editorRef) || null;
};

View File

@@ -263,7 +263,7 @@ export const buildMainMenuDeclarativeTemplate = ({
? []
: [
{
label: i18n._(t`Ask AI (GDevelop chatbot)`),
label: i18n._(t`Ask AI (AI agent and chatbot)`),
onClickSendEvent: 'main-menu-open-ask-ai',
},
]),

View File

@@ -49,6 +49,7 @@ import {
moveTabToTheRightOfHoveredTab,
getCustomObjectEditor,
hasEditorTabOpenedWithKey,
getOpenedAskAiEditor,
} from './EditorTabs/EditorTabsHandler';
import { renderDebuggerEditorContainer } from './EditorContainers/DebuggerEditorContainer';
import { renderEventsEditorContainer } from './EditorContainers/EventsEditorContainer';
@@ -1226,13 +1227,21 @@ const MainFrame = (props: Props) => {
const openAskAi = React.useCallback(
() => {
setState(state => ({
...state,
editorTabs: openEditorTab(
state.editorTabs,
getEditorOpeningOptions({ kind: 'ask-ai', name: '' })
),
}));
setState(state => {
const askAiEditor = getOpenedAskAiEditor(state.editorTabs);
if (askAiEditor) {
askAiEditor.startNewChat();
}
// Open or focus the AI editor.
return {
...state,
editorTabs: openEditorTab(
state.editorTabs,
getEditorOpeningOptions({ kind: 'ask-ai', name: '' })
),
};
});
},
[setState, getEditorOpeningOptions]
);
@@ -1284,28 +1293,6 @@ const MainFrame = (props: Props) => {
toolbar.current.setEditorToolbar(editorToolbar);
};
const onInstallExtension = (extensionName: string) => {
const { currentProject } = state;
if (!currentProject) return;
// Close the extension tab before updating/reinstalling the extension.
const eventsFunctionsExtensionName = extensionName;
if (
currentProject.hasEventsFunctionsExtensionNamed(
eventsFunctionsExtensionName
)
) {
setState(state => ({
...state,
editorTabs: closeEventsFunctionsExtensionTabs(
state.editorTabs,
eventsFunctionsExtensionName
),
}));
}
};
const deleteLayout = (layout: gdLayout) => {
const { currentProject } = state;
const { i18n } = props;
@@ -1412,31 +1399,60 @@ const MainFrame = (props: Props) => {
});
};
const onExtensionInstalled = (extensionName: string) => {
const onInstallExtension = (extensionName: string) => {
const { currentProject } = state;
if (!currentProject) return;
// Close the extension tab before updating/reinstalling the extension.
// This is especially important when the extension tab in selected.
const eventsFunctionsExtensionName = extensionName;
if (
currentProject.hasEventsFunctionsExtensionNamed(
eventsFunctionsExtensionName
)
) {
setState(state => ({
...state,
editorTabs: closeEventsFunctionsExtensionTabs(
state.editorTabs,
eventsFunctionsExtensionName
),
}));
}
};
const onExtensionInstalled = (extensionNames: Array<string>) => {
const { currentProject } = state;
if (!currentProject) {
return;
}
const eventsBasedObjects = currentProject
.getEventsFunctionsExtension(extensionName)
.getEventsBasedObjects();
for (let index = 0; index < eventsBasedObjects.getCount(); index++) {
const eventsBasedObject = eventsBasedObjects.getAt(index);
gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject(
currentProject,
eventsBasedObject
);
for (const extensionName of extensionNames) {
const eventsBasedObjects = currentProject
.getEventsFunctionsExtension(extensionName)
.getEventsBasedObjects();
for (let index = 0; index < eventsBasedObjects.getCount(); index++) {
const eventsBasedObject = eventsBasedObjects.getAt(index);
gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject(
currentProject,
eventsBasedObject
);
}
// Close extension tab because `onInstallExtension` is not necessarily
// called when the extension tab is not selected.
// TODO Open the closed tabs back
// It would be safer to close the tabs before the extension is installed
// but it would make opening them back more complicated.
setState(state => ({
...state,
editorTabs: closeEventsFunctionsExtensionTabs(
state.editorTabs,
extensionName
),
}));
}
// TODO Open the closed tabs back
// It would be safer to close the tabs before the extension is installed
// but it would make opening them back more complicated.
setState(state => ({
...state,
editorTabs: closeEventsFunctionsExtensionTabs(
state.editorTabs,
extensionName
),
}));
};
const renameLayout = (oldName: string, newName: string) => {
@@ -1916,16 +1932,32 @@ const MainFrame = (props: Props) => {
{
openEventsEditor,
openSceneEditor,
}: {| openEventsEditor: boolean, openSceneEditor: boolean |}
focusWhenOpened,
}: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|}
): EditorTabsState => {
const sceneEditorOptions = getEditorOpeningOptions({
kind: 'layout',
name,
dontFocusTab: !(
focusWhenOpened === 'scene' ||
focusWhenOpened === 'scene-or-events-otherwise'
),
});
const eventsEditorOptions = getEditorOpeningOptions({
kind: 'layout events',
name,
dontFocusTab: openSceneEditor,
dontFocusTab: !(
focusWhenOpened === 'events' ||
(focusWhenOpened === 'scene-or-events-otherwise' && !openSceneEditor)
),
});
const tabsWithSceneEditor = openSceneEditor
@@ -1941,9 +1973,18 @@ const MainFrame = (props: Props) => {
const openLayout = React.useCallback(
(
name: string,
options?: {| openEventsEditor: boolean, openSceneEditor: boolean |} = {
options?: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|} = {
openEventsEditor: true,
openSceneEditor: true,
focusWhenOpened: 'scene',
},
editorTabs?: EditorTabsState
): void => {
@@ -1955,6 +1996,7 @@ const MainFrame = (props: Props) => {
{
openEventsEditor: options.openEventsEditor,
openSceneEditor: options.openSceneEditor,
focusWhenOpened: options.focusWhenOpened,
}
),
}));
@@ -2450,6 +2492,7 @@ const MainFrame = (props: Props) => {
{
openSceneEditor: true,
openEventsEditor: true,
focusWhenOpened: 'scene',
},
editorTabs
);
@@ -2483,6 +2526,7 @@ const MainFrame = (props: Props) => {
{
openSceneEditor: true,
openEventsEditor: true,
focusWhenOpened: 'scene',
}
);
}
@@ -2513,6 +2557,7 @@ const MainFrame = (props: Props) => {
openLayout(firstLayout, {
openSceneEditor: true,
openEventsEditor: true,
focusWhenOpened: 'scene',
});
setIsLoadingProject(false);
@@ -3973,14 +4018,10 @@ const MainFrame = (props: Props) => {
openLayout(sceneName, {
openEventsEditor: true,
openSceneEditor: false,
focusWhenOpened: 'events',
});
},
onOpenLayout: (sceneName: string) => {
openLayout(sceneName, {
openEventsEditor: false,
openSceneEditor: true,
});
},
onOpenLayout: openLayout,
onOpenTemplateFromTutorial: openTemplateFromTutorial,
onOpenTemplateFromCourseChapter: openTemplateFromCourseChapter,
previewDebuggerServer,
@@ -4395,6 +4436,7 @@ const MainFrame = (props: Props) => {
currentProject.getProjectUuid()
)}
onScreenshotsClaimed={onGameScreenshotsClaimed}
onExtensionInstalled={onExtensionInstalled}
/>
)}
<CustomDragLayer />

View File

@@ -215,7 +215,7 @@ type Props = {|
objects: Array<gdObject>,
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
isVariableListLocked: boolean,
isBehaviorListLocked: boolean,
|};

View File

@@ -401,10 +401,11 @@ const CustomObjectPropertiesEditor = (props: Props) => {
</ColumnStackLayout>
</>
)}
{!getVariantName(
{(!getVariantName(
eventBasedObject,
customObjectConfiguration
) &&
) ||
customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration()) &&
(eventBasedObject &&
(!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() &&
!customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? (

View File

@@ -60,7 +60,7 @@ type Props = {|
// Preview:
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
openBehaviorEvents: (extensionName: string, behaviorName: string) => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
onOpenEventBasedObjectEditor: (
extensionName: string,
eventsBasedObjectName: string

View File

@@ -479,6 +479,7 @@ type Props = {|
onObjectPasted?: gdObject => void,
getValidatedObjectOrGroupName: (newName: string, global: boolean) => string,
onAddObjectInstance: (objectName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
getThumbnail: (
project: gdProject,
@@ -518,6 +519,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
onObjectPasted,
getValidatedObjectOrGroupName,
onAddObjectInstance,
onExtensionInstalled,
getThumbnail,
unsavedChanges,
@@ -1591,6 +1593,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
objectsContainer={objectsContainer}
resourceManagementProps={resourceManagementProps}
targetObjectFolderOrObjectWithContext={newObjectDialogOpen.from}
onExtensionInstalled={onExtensionInstalled}
/>
)}
{objectAssetSwappingDialogOpen && (
@@ -1606,6 +1609,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
objectsContainer={objectsContainer}
object={objectAssetSwappingDialogOpen.objectWithContext.object}
resourceManagementProps={resourceManagementProps}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</Background>

View File

@@ -26,6 +26,32 @@ const getVariant = (
: eventBasedObject.getDefaultVariant();
};
const getChildObjectConfiguration = (
childObjectName: string,
eventBasedObject: gdEventsBasedObject,
customObjectConfiguration: gdCustomObjectConfiguration,
variant: gdEventsBasedObjectVariant
): gdObjectConfiguration | null => {
// Legacy events-based objects don't have any instance in their default
// variant since there wasn't a graphical editor at the time.
// In this case, the editor doesn't allow to choose a variant, but a
// variant may have stayed after a user rolled back the extension.
// This variant must be ignored to match what the editor shows.
if (
customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() ||
(variant === eventBasedObject.getDefaultVariant() &&
customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration())
) {
return customObjectConfiguration.getChildObjectConfiguration(
childObjectName
);
}
const childObjects = variant.getObjects();
return childObjects.hasObjectNamed(childObjectName)
? childObjects.getObject(childObjectName).getConfiguration()
: null;
};
type PropertyMappingRule = {
targetChild: string,
targetProperty: string,
@@ -189,6 +215,28 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
};
}
_getChildObjectConfiguration = (
childObjectName: string
): gdObjectConfiguration | null => {
const eventBasedObject = this.eventBasedObject;
if (!eventBasedObject) {
return null;
}
const customObjectConfiguration = gd.asCustomObjectConfiguration(
this._associatedObjectConfiguration
);
const variant = getVariant(eventBasedObject, customObjectConfiguration);
if (!variant) {
return null;
}
return getChildObjectConfiguration(
childObjectName,
eventBasedObject,
customObjectConfiguration,
variant
);
};
getRendererOfInstance = (
instance: gdInitialInstance
): RenderedInstance | Rendered3DInstance => {
@@ -199,23 +247,6 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
const customObjectConfiguration = gd.asCustomObjectConfiguration(
this._associatedObjectConfiguration
);
let childObjectConfiguration = null;
const variant = this.getVariant();
if (variant) {
const childObjects = variant.getObjects();
if (childObjects.hasObjectNamed(instance.getObjectName())) {
const childObject = childObjects.getObject(instance.getObjectName());
childObjectConfiguration =
this.eventBasedObject &&
customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() &&
variant === this.eventBasedObject.getDefaultVariant()
? customObjectConfiguration.getChildObjectConfiguration(
instance.getObjectName()
)
: childObject.getConfiguration();
}
}
// Apply property mapping rules on the child instance.
const childPropertyOverridings = new Map<string, string>();
const customObjectProperties = customObjectConfiguration.getProperties();
@@ -238,6 +269,9 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
}
}
//...so let's create a renderer.
const childObjectConfiguration = this._getChildObjectConfiguration(
instance.getObjectName()
);
renderedInstance = childObjectConfiguration
? ObjectsRenderingService.createNewInstanceRenderer(
this._project,
@@ -362,16 +396,16 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
const childObjects = variant.getObjects();
for (let i = 0; i < childObjects.getObjectsCount(); i++) {
const childObject = childObjects.getObjectAt(i);
const childObjectConfiguration =
customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() ||
customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration()
? customObjectConfiguration.getChildObjectConfiguration(
childObject.getName()
)
: variant
.getObjects()
.getObject(childObject.getName())
.getConfiguration();
const childObjectConfiguration = getChildObjectConfiguration(
childObject.getName(),
eventBasedObject,
customObjectConfiguration,
variant
);
if (!childObjectConfiguration) {
continue;
}
const childType = childObjectConfiguration.getType();
if (
childType === 'Sprite' ||

View File

@@ -173,6 +173,7 @@ const CurrentUsageDisplayer = ({
}
hideButton={cannotUpgradeSubscription}
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="builds"
>
<Line>
{!isFeatureLocked ? (
@@ -219,6 +220,7 @@ const CurrentUsageDisplayer = ({
}
}
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="builds"
>
<Line>
{!isFeatureLocked ? (

View File

@@ -3,7 +3,10 @@ import * as React from 'react';
import { Trans } from '@lingui/macro';
import { Column, Line } from '../../UI/Grid';
import { ResponsiveLineStackLayout } from '../../UI/Layout';
import { type SubscriptionDialogDisplayReason } from '../../Utils/Analytics/EventSender';
import {
type SubscriptionDialogDisplayReason,
type SubscriptionPlacementId,
} from '../../Utils/Analytics/EventSender';
import { SubscriptionSuggestionContext } from './SubscriptionSuggestionContext';
import RaisedButton from '../../UI/RaisedButton';
import FlatButton from '../../UI/FlatButton';
@@ -53,6 +56,7 @@ type Props = {|
| 'gdevelop_startup'
| 'gdevelop_education',
canHide?: boolean,
placementId: SubscriptionPlacementId,
|};
const GetSubscriptionCard = ({
@@ -66,6 +70,7 @@ const GetSubscriptionCard = ({
filter,
recommendedPlanIdIfNoSubscription,
canHide,
placementId,
}: Props) => {
const [isHidden, setIsHidden] = React.useState(false);
const { subscription } = React.useContext(AuthenticatedUserContext);
@@ -116,6 +121,7 @@ const GetSubscriptionCard = ({
analyticsMetadata: {
reason: subscriptionDialogOpeningReason,
recommendedPlanId: actualPlanIdToRecommend,
placementId,
},
filter,
});

View File

@@ -16,6 +16,7 @@ import { isNativeMobileApp } from '../../Utils/Platform';
import InAppTutorialContext from '../../InAppTutorial/InAppTutorialContext';
import GetSubscriptionCard from './GetSubscriptionCard';
import { ColumnStackLayout } from '../../UI/Layout';
import { type SubscriptionPlacementId } from '../../Utils/Analytics/EventSender';
export type SubscriptionCheckerInterface = {|
checkUserHasSubscription: () => boolean,
@@ -29,6 +30,7 @@ type Props = {|
| 'Debugger'
| 'Hot reloading'
| 'Preview over wifi',
placementId: SubscriptionPlacementId,
onChangeSubscription?: () => Promise<void> | void,
mode: 'try' | 'mandatory',
isNotShownDuringInAppTutorial?: boolean,
@@ -39,7 +41,14 @@ const SubscriptionChecker = React.forwardRef<
SubscriptionCheckerInterface
>(
(
{ mode, id, title, onChangeSubscription, isNotShownDuringInAppTutorial },
{
mode,
id,
title,
onChangeSubscription,
placementId,
isNotShownDuringInAppTutorial,
},
ref
) => {
const authenticatedUser = React.useContext(AuthenticatedUserContext);
@@ -122,6 +131,7 @@ const SubscriptionChecker = React.forwardRef<
setDialogOpen(false);
}}
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId={placementId}
>
<Column noMargin expand>
<Text>

View File

@@ -341,7 +341,10 @@ const SubscriptionDetails = ({
primary
onClick={() => {
openSubscriptionDialog({
analyticsMetadata: { reason: 'Consult profile' },
analyticsMetadata: {
reason: 'Consult profile',
placementId: 'profile',
},
});
}}
disabled={isManageSubscriptionLoading}
@@ -390,6 +393,7 @@ const SubscriptionDetails = ({
<GetSubscriptionCard
label={<Trans>Choose a subscription</Trans>}
subscriptionDialogOpeningReason="Consult profile"
placementId="profile"
>
<Text noMargin>
<Trans>
@@ -440,7 +444,10 @@ const SubscriptionDetails = ({
color={buttonColor}
onClick={() =>
openSubscriptionDialog({
analyticsMetadata: { reason: 'Consult profile' },
analyticsMetadata: {
reason: 'Consult profile',
placementId: 'profile',
},
filter: key,
})
}
@@ -458,6 +465,7 @@ const SubscriptionDetails = ({
label={<Trans>Choose a subscription</Trans>}
subscriptionDialogOpeningReason="Consult profile"
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="profile"
>
<Text noMargin>
<Trans>

View File

@@ -5,6 +5,7 @@ import SubscriptionDialog from './SubscriptionDialog';
import {
sendSubscriptionDialogShown,
type SubscriptionDialogDisplayReason,
type SubscriptionPlacementId,
} from '../../Utils/Analytics/EventSender';
import { isNativeMobileApp } from '../../Utils/Platform';
import {
@@ -26,6 +27,7 @@ export type SubscriptionType = 'individual' | 'team' | 'education';
export type SubscriptionAnalyticsMetadata = {|
reason: SubscriptionDialogDisplayReason,
recommendedPlanId?: string,
placementId: SubscriptionPlacementId,
preStep?: 'subscriptionChecker',
|};

View File

@@ -238,6 +238,7 @@ export const LoadingScreenEditor = ({
<GetSubscriptionCard
subscriptionDialogOpeningReason="Disable GDevelop splash at startup"
recommendedPlanIdIfNoSubscription="gdevelop_silver"
placementId="gdevelop-branding"
>
<Text>
<Trans>
@@ -555,6 +556,7 @@ export const LoadingScreenEditor = ({
mode="mandatory"
id="Disable GDevelop splash at startup"
title={<Trans>Disable GDevelop splash at startup</Trans>}
placementId="gdevelop-branding"
/>
</ColumnStackLayout>
)}

View File

@@ -29,6 +29,11 @@ export type SceneTreeViewItemCallbacks = {|
options?: {|
openEventsEditor: boolean,
openSceneEditor: boolean,
focusWhenOpened:
| 'scene-or-events-otherwise'
| 'scene'
| 'events'
| 'none',
|}
) => void,
|};
@@ -91,7 +96,11 @@ export class SceneTreeViewItemContent implements TreeViewItemContent {
}
onClick(): void {
this.props.onOpenLayout(this.scene.getName());
this.props.onOpenLayout(this.scene.getName(), {
openEventsEditor: true,
openSceneEditor: true,
focusWhenOpened: 'scene',
});
}
rename(newName: string): void {
@@ -115,6 +124,7 @@ export class SceneTreeViewItemContent implements TreeViewItemContent {
this.props.onOpenLayout(this.scene.getName(), {
openSceneEditor: true,
openEventsEditor: false,
focusWhenOpened: 'scene',
}),
},
{
@@ -124,6 +134,7 @@ export class SceneTreeViewItemContent implements TreeViewItemContent {
this.props.onOpenLayout(this.scene.getName(), {
openSceneEditor: false,
openEventsEditor: true,
focusWhenOpened: 'events',
}),
},
{

View File

@@ -422,7 +422,7 @@ type Props = {|
onShareProject: () => void,
onOpenHomePage: () => void,
toggleProjectManager: () => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
// Main menu
mainMenuCallbacks: MainMenuCallbacks,

View File

@@ -31,6 +31,7 @@ type Props = {|
sourceGameId: string,
gameScreenshotUrls: Array<string>,
onScreenshotsClaimed: () => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
export const QuickCustomizationDialog = ({
@@ -46,6 +47,7 @@ export const QuickCustomizationDialog = ({
sourceGameId,
gameScreenshotUrls,
onScreenshotsClaimed,
onExtensionInstalled,
}: Props) => {
const { triggerUnsavedChanges } = React.useContext(UnsavedChangesContext);
const gameAndBuildsManager = useGameAndBuildsManager({
@@ -75,6 +77,7 @@ export const QuickCustomizationDialog = ({
onContinueQuickCustomization,
gameScreenshotUrls,
onScreenshotsClaimed,
onExtensionInstalled,
});
const name = project.getName();

View File

@@ -14,6 +14,7 @@ import TipCard from './TipCard';
type Props = {|
project: gdProject,
resourceManagementProps: ResourceManagementProps,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
const styles = {
@@ -29,6 +30,7 @@ const styles = {
export const QuickObjectReplacer = ({
project,
resourceManagementProps,
onExtensionInstalled,
}: Props) => {
const [selectedObjectToSwap, setSelectedObjectToSwap] = React.useState(null);
@@ -96,6 +98,7 @@ export const QuickObjectReplacer = ({
setSelectedObjectToSwap(null);
}}
minimalUI
onExtensionInstalled={onExtensionInstalled}
/>
)}
</ColumnStackLayout>

View File

@@ -149,6 +149,7 @@ type Props = {|
onContinueQuickCustomization: () => void,
gameScreenshotUrls: Array<string>,
onScreenshotsClaimed: () => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
|};
export const renderQuickCustomization = ({
@@ -165,6 +166,7 @@ export const renderQuickCustomization = ({
onContinueQuickCustomization,
gameScreenshotUrls,
onScreenshotsClaimed,
onExtensionInstalled,
}: Props) => {
return {
title: quickCustomizationState.step.title,
@@ -174,6 +176,7 @@ export const renderQuickCustomization = ({
<QuickObjectReplacer
project={project}
resourceManagementProps={resourceManagementProps}
onExtensionInstalled={onExtensionInstalled}
/>
) : quickCustomizationState.step.name === 'tweak-behaviors' ? (
<QuickBehaviorsTweaker

View File

@@ -156,7 +156,7 @@ export const ProjectResourceCard = ({
>
{renderResourcePreview()}
<div style={styles.titleContainer}>
<Text noMargin style={styles.title}>
<Text noMargin style={styles.title} color="inherit">
{resourceName}
</Text>
</div>

View File

@@ -98,7 +98,7 @@ export type SceneEditorsDisplayProps = {|
i18n: I18nType,
objectOrGroupName: string
) => boolean,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
updateBehaviorsSharedData: () => void,
onInstancesAdded: (Array<gdInitialInstance>) => void,

View File

@@ -42,7 +42,7 @@ type Props = {|
objects: Array<gdObject>,
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
onUpdateBehaviorsSharedData: () => void,
onExtensionInstalled: (extensionName: string) => void,
onExtensionInstalled: (extensionNames: Array<string>) => void,
isBehaviorListLocked: boolean,
// For instances:

View File

@@ -96,6 +96,7 @@ const MosaicEditorsDisplay = React.forwardRef<
initialInstances,
selectedLayer,
onSelectInstances,
onExtensionInstalled,
} = props;
const { isMobile } = useResponsiveWindowSize();
const {
@@ -427,6 +428,7 @@ const MosaicEditorsDisplay = React.forwardRef<
unsavedChanges={props.unsavedChanges}
hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps}
isListLocked={isCustomVariant}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</I18n>

View File

@@ -72,6 +72,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
initialInstances,
selectedLayer,
onSelectInstances,
onExtensionInstalled,
} = props;
const selectedInstances = props.instancesSelection.getSelectedInstances();
const { values } = React.useContext(PreferencesContext);
@@ -373,6 +374,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
props.hotReloadPreviewButtonProps
}
isListLocked={isCustomVariant}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</I18n>

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