chore(sync): sync upstream 4ian/GDevelop changes

Merge 4ian/GDevelop master (7a7736a3a4) into the Heretek fork.

- Resolve conflicts in README.md, newIDE/app/package.json and
  AiRequestChat (ChatMessages.js, index.js), re-applying BYOK
  behavior on top of upstream's AiUsageIndicator refactor.
- Keep Heretek Vite/prettier TS scripts alongside upstream's
  check-build-output.js build checks.
- Fix BYOK event-generation path after upstream dropped
  existingEventsAsText, now derived from existingEventsJson.
- Lock .github/workflows to master to prevent upstream workflow
  tampering.
This commit is contained in:
John Smith
2026-09-12 20:41:39 -04:00
499 changed files with 34781 additions and 6495 deletions
+381 -4
View File
@@ -9,11 +9,50 @@
# to avoid sharing secrets.
version: 2.1
parameters:
# Run *every* visual monkey test, even on a branch. Set it when triggering a
# pipeline (CircleCI UI or API) to check everything without waiting for the
# change to land on `master`.
run-all-visual-tests:
type: boolean
default: false
# Number of parallel containers the visual monkey tests are split across.
all-visual-tests-parallelism:
type: integer
default: 4
# Same, for a branch (which only runs the tests related to its changes).
changed-visual-tests-parallelism:
type: integer
default: 2
orbs:
aws-cli: circleci/[email protected]
macos: circleci/[email protected] # For Rosetta (see below)
node: circleci/[email protected] # For a recent npm version (see below)
win: circleci/[email protected]
commands:
# Used by the visual monkey tests jobs on a "changed" scope: stop the job
# (before anything expensive) when no test of the suite is related to what
# the branch changes. A change to the tests themselves relates every test.
halt-unless-a-visual-test-is-related-to-the-changes:
parameters:
scope:
type: enum
enum: ['all', 'changed']
suite:
type: enum
enum: ['storybook', 'editor']
steps:
- run:
name: Stop here if no test is related to the changes
command: |
if [ "<< parameters.scope >>" != "changed" ]; then exit 0; fi
git fetch --depth=50 origin master
cd newIDE/visual-tests
PUPPETEER_SKIP_DOWNLOAD=1 npm install --no-audit --no-fund --silent
if [ -z "$(node run.js --suite=<< parameters.suite >> --list-names --only-changed --base-ref=origin/master)" ]; then
echo "No visual monkey test of the << parameters.suite >> suite is related to these changes."
circleci-agent step halt
fi
jobs:
# Build the **entire** app for macOS (including the GDevelop.js library).
build-macos:
@@ -715,6 +754,268 @@ jobs:
path: ./cli-html5-export-artifacts
destination: cli-html5-export-artifacts
# Visual monkey tests: manipulate an editor in a real browser and check that
# nothing is broken (see newIDE/visual-tests/README.md).
#
# The "storybook" suite needs a built Storybook, which is built once here and
# shared with the containers running the tests.
build-storybook-for-visual-tests:
parameters:
scope:
description: >
"all" runs every test. "changed" only runs the tests related to what
the branch changes - and nothing at all if it changes nothing related.
type: enum
enum: ['all', 'changed']
default: 'all'
resource_class: large
docker:
- image: cimg/node:24.13.0
working_directory: ~/GDevelop
steps:
- checkout
- halt-unless-a-visual-test-is-related-to-the-changes:
scope: << parameters.scope >>
suite: 'storybook'
- restore_cache:
keys:
- gd-linux-nodejs-dependencies-v2-{{ checksum "newIDE/app/package.json" }}-{{ checksum "newIDE/electron-app/package.json" }}-{{ checksum "GDevelop.js/package.json" }}-{{ checksum "GDJS/package-lock.json" }}
# fallback to using the latest cache if no exact match is found
- gd-linux-nodejs-dependencies-v2-
- run:
name: Install GDevelop IDE dependencies
command: cd newIDE/app && npm install
- run:
name: Build Storybook
no_output_timeout: 20m
command: export NODE_OPTIONS="--max-old-space-size=7168" && cd newIDE/app && npm run build-storybook
- persist_to_workspace:
root: newIDE/app
paths:
- build-storybook
run-visual-monkey-tests-storybook-linux:
parameters:
scope:
type: enum
enum: ['all', 'changed']
default: 'all'
parallelism:
description: Number of containers the tests are split across.
type: integer
default: 1
parallelism: << parameters.parallelism >>
resource_class: large
docker:
- image: cimg/node:24.13.0
working_directory: ~/GDevelop
steps:
- checkout
- halt-unless-a-visual-test-is-related-to-the-changes:
scope: << parameters.scope >>
suite: 'storybook'
- attach_workspace:
at: newIDE/app
# The tests run in the Chrome that `npm install` downloads for Puppeteer
# (see newIDE/visual-tests/package.json), which needs these libraries.
- run:
name: Install the libraries Chrome needs
command: |
sudo apt-get update
sudo apt-get install -y \
libnss3 \
libasound2t64 \
libgbm1 \
libgtk-3-0 \
libxss1 \
libatk-bridge2.0-0 \
libdrm2 \
libxkbcommon0
- restore_cache:
keys:
- gd-visual-tests-dependencies-v1-{{ checksum "newIDE/visual-tests/package.json" }}
- run:
name: Install the visual tests dependencies (and the Chrome to run them in)
# The install of "Stop here if no test is related to the changes"
# skipped the Chrome download, and npm does not re-run the postinstall
# of an already installed Puppeteer: run it explicitly (it does
# nothing when Chrome is already in the restored cache).
command: |
cd newIDE/visual-tests
npm install --no-audit --no-fund
node node_modules/puppeteer/install.mjs
- save_cache:
paths:
- newIDE/visual-tests/node_modules
- ~/.cache/puppeteer
key: gd-visual-tests-dependencies-v1-{{ checksum "newIDE/visual-tests/package.json" }}
- run:
name: List the tests to run
command: |
cd newIDE/visual-tests
if [ "<< parameters.scope >>" = "changed" ]; then
node run.js --suite=storybook --list-names --only-changed --base-ref=origin/master > /tmp/tests.txt
else
node run.js --suite=storybook --list-names > /tmp/tests.txt
fi
echo "Tests to run:"
cat /tmp/tests.txt
- run:
name: Split the tests across the parallel containers
# Balanced using the durations recorded by `store_test_results` on the
# previous runs (falls back to an even split the first time).
command: |
if [ -s /tmp/tests.txt ]; then
circleci tests split --split-by=timings --timings-type=filename \
< /tmp/tests.txt > /tmp/tests-for-this-container.txt
else
: > /tmp/tests-for-this-container.txt
fi
echo "Tests for this container:"
cat /tmp/tests-for-this-container.txt
- run:
name: Run the visual monkey tests on Storybook
no_output_timeout: 30m
# The failure is reported by the last step of the job, so that the
# results and the screenshots of a failing run are uploaded first.
command: |
set +e
cd newIDE/visual-tests
node run.js --suite=storybook \
--tests-file=/tmp/tests-for-this-container.txt \
--artifacts-dir=/tmp/visual-tests-artifacts \
--junit-path=/tmp/visual-tests-results/results.xml
echo $? > /tmp/visual-tests-exit-code
- store_test_results:
path: /tmp/visual-tests-results
- store_artifacts:
path: /tmp/visual-tests-artifacts
destination: visual-tests
- run:
name: Report whether the visual monkey tests passed
command: exit "$(cat /tmp/visual-tests-exit-code)"
# Visual monkey tests, "editor" suite: smoke tests using a real, packaged
# version of the app (like run-cli-html5-export-linux does) opening a real
# example game.
#
# Without a local build (so on a branch), the tests run against the latest
# portable build of `master` on S3: they then exercise the tests of the
# branch, not its changes to the app itself - those are only tested by the
# jobs following build-linux, on the branches building the app.
run-visual-monkey-tests-editor-linux:
parameters:
scope:
description: >
"all" runs every test. "changed" only runs the tests related to what
the branch changes - and nothing at all if it changes nothing related.
type: enum
enum: ['all', 'changed']
default: 'all'
use-local-build:
description: When true, attach the workspace produced by build-linux and use its portable zip instead of downloading the latest one from S3.
type: boolean
default: false
resource_class: medium
docker:
- image: cimg/node:24.13.0
working_directory: ~/GDevelop
steps:
- checkout
- halt-unless-a-visual-test-is-related-to-the-changes:
scope: << parameters.scope >>
suite: 'editor'
- when:
condition: << parameters.use-local-build >>
steps:
- attach_workspace:
at: ~/GDevelop/build-linux-workspace
# Runtime libraries required by Electron (headless still needs nss /
# alsa to even start up).
- run:
name: Install Electron runtime dependencies
command: |
sudo apt-get update
sudo apt-get install -y \
libnss3 \
libasound2t64 \
libgbm1 \
libgtk-3-0 \
libxss1 \
libatk-bridge2.0-0 \
libdrm2 \
libxkbcommon0 \
xvfb
- run:
name: Install the visual tests dependencies
command: cd newIDE/visual-tests && npm install --no-audit --no-fund
- run:
name: Run the visual monkey tests on the real editor
# The editor is a GUI app: it needs a display, even to run headless.
# The failure is reported by the last step of the job, so that the
# results and the screenshots of a failing run are uploaded first.
no_output_timeout: 30m
command: |
set +e
cd newIDE/visual-tests
if [ "<< parameters.use-local-build >>" = "true" ]; then
ZIP_PATH=$(ls ~/GDevelop/build-linux-workspace/gdevelop-*.zip | grep -v -- '-arm64' | head -1)
if [ -z "$ZIP_PATH" ]; then
echo "No portable zip found in the build-linux workspace." >&2
exit 1
fi
echo "Using the locally built portable zip: $ZIP_PATH"
EXTRA_ARGS="--gdevelop-zip=$ZIP_PATH"
else
echo "No local build available; the latest portable zip of master will be downloaded."
EXTRA_ARGS="--gdevelop-branch=master"
fi
if [ "<< parameters.scope >>" = "changed" ]; then
EXTRA_ARGS="$EXTRA_ARGS --only-changed --base-ref=origin/master"
fi
xvfb-run -a --server-args="-screen 0 1500x1000x24" \
node run.js --suite=editor $EXTRA_ARGS \
--artifacts-dir=/tmp/visual-tests-artifacts \
--junit-path=/tmp/visual-tests-results/results.xml
echo $? > /tmp/visual-tests-exit-code
- store_test_results:
path: /tmp/visual-tests-results
- store_artifacts:
path: /tmp/visual-tests-artifacts
destination: visual-tests
- run:
name: Report whether the visual monkey tests passed
command: exit "$(cat /tmp/visual-tests-exit-code)"
workflows:
gdevelop_js-wasm-extra-checks:
jobs:
@@ -766,6 +1067,19 @@ workflows:
- master
- stable
- /experimental-build.*/
# Right after build-linux, run the visual monkey tests of the real
# editor against the portable zip it just produced.
- run-visual-monkey-tests-editor-linux:
name: visual-monkey-tests-editor-linux
use-local-build: true
requires:
- build-linux
filters:
branches:
only:
- master
- stable
- /experimental-build.*/
- build-windows:
requires:
- build-gdevelop_js-wasm-only
@@ -786,10 +1100,10 @@ workflows:
# - stable
# - /experimental-build.*/
# Daily end-to-end test of the GDevelop CLI HTML5 export. Runs every day
# at 16:00 UTC (4pm UTC) against the latest portable build uploaded to S3
# for master.
daily-cli-html5-export-linux:
# Daily end-to-end tests running against the latest portable build uploaded
# to S3 for master: the CLI HTML5 export, and the visual monkey tests
# manipulating the editor. Runs every day at 16:00 UTC (4pm UTC).
daily-end-to-end-tests:
triggers:
- schedule:
cron: "0 16 * * *"
@@ -799,3 +1113,66 @@ workflows:
- master
jobs:
- run-cli-html5-export-linux
- run-visual-monkey-tests-editor-linux
# On `master`/`stable` (or when explicitly asked for): manipulate the editors
# in a real browser and check that nothing is broken, with every test.
# (`master`, `stable` and the experimental builds also run the editor suite
# against the build build-linux just produced, in the main workflow.)
all-visual-tests:
when:
or:
- equal: [master, << pipeline.git.branch >>]
- equal: [stable, << pipeline.git.branch >>]
- << pipeline.parameters.run-all-visual-tests >>
jobs:
- build-storybook-for-visual-tests:
name: visual-tests-storybook-build-all
scope: 'all'
- run-visual-monkey-tests-storybook-linux:
name: visual-tests-storybook-all
scope: 'all'
parallelism: << pipeline.parameters.all-visual-tests-parallelism >>
requires:
- visual-tests-storybook-build-all
# On a branch forced with `run-all-visual-tests`: every test of the
# editor suite too, against the latest portable build of `master`.
- run-visual-monkey-tests-editor-linux:
name: visual-tests-editor-all
scope: 'all'
filters:
branches:
ignore:
- master
- stable
- /experimental-build.*/
# On a branch / Pull Request: only run the tests related to what it changes
# (nothing at all when it changes nothing related to them). The editor suite
# runs against the latest portable build of `master` (a branch builds no
# app), so it exercises the tests of the branch, not its changes to the app.
changed-visual-tests:
when:
and:
- not:
equal: [master, << pipeline.git.branch >>]
- not:
equal: [stable, << pipeline.git.branch >>]
- not: << pipeline.parameters.run-all-visual-tests >>
jobs:
- build-storybook-for-visual-tests:
name: visual-tests-storybook-build-changed
scope: 'changed'
- run-visual-monkey-tests-storybook-linux:
name: visual-tests-storybook-changed
scope: 'changed'
parallelism: << pipeline.parameters.changed-visual-tests-parallelism >>
requires:
- visual-tests-storybook-build-changed
- run-visual-monkey-tests-editor-linux:
name: visual-tests-editor-changed
scope: 'changed'
filters:
branches:
ignore:
- /experimental-build.*/
@@ -533,7 +533,7 @@ class GD_CORE_API EventsCodeGenerator {
* - object : Object name -> string
* - expression : Mathematical expression -> number (double)
* - string : %Text expression -> string
* - layer, color, file, stringWithSelector : Same as string
* - layer, color, file, stringWithSelector, easing : Same as string
* - relationalOperator : Used to make a comparison between the function
return value and value of the parameter preceding the relationOperator
parameter -> string
@@ -55,6 +55,7 @@ gd::String ExpressionCodeGenerator::GenerateExpressionCode(
gd::ExpressionValidator validator(codeGenerator.GetPlatform(),
codeGenerator.GetProjectScopedContainers(),
rootType,
rootObjectName,
extraInfo);
node->Visit(validator);
if (!validator.GetFatalErrors().empty()) {
+1 -1
View File
@@ -53,7 +53,7 @@ void EventsList::InsertEvent(std::shared_ptr<gd::BaseEvent> event,
events.push_back(event);
}
gd::BaseEvent& EventsList::InsertNewEvent(gd::Project& project,
gd::BaseEvent& EventsList::InsertNewEvent(const gd::Project& project,
const gd::String& eventType,
size_t position) {
gd::BaseEventSPtr event = project.CreateEvent(eventType);
+1 -1
View File
@@ -66,7 +66,7 @@ class GD_CORE_API EventsList {
* \param position Insertion position. If the position is invalid, the object
* is inserted at the end of the objects list.
*/
gd::BaseEvent& InsertNewEvent(gd::Project& project,
gd::BaseEvent& InsertNewEvent(const gd::Project& project,
const gd::String& eventType,
size_t position = (size_t)-1);
@@ -179,6 +179,10 @@ class GD_CORE_API ValueTypeMetadata {
return type == "scenevar" || type == "globalvar" || type == "objectvar";
}
static bool IsTypeObjectVariable(const gd::String &type) {
return type == "objectvar";
}
/**
* \brief Return true if the type is representing one object
* (or more, i.e: an object group).
@@ -223,6 +227,7 @@ class GD_CORE_API ValueTypeMetadata {
return parameterType == "string" || parameterType == "layer" ||
parameterType == "color" || parameterType == "file" ||
parameterType == "stringWithSelector" ||
parameterType == "easing" ||
parameterType == "sceneName" ||
parameterType == "layerEffectName" ||
parameterType == "layerEffectParameterName" ||
@@ -15,7 +15,6 @@
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
@@ -15,7 +15,6 @@
#include "GDCore/Extensions/Metadata/InstructionMetadata.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/IDE/Events/ExpressionTypeFinder.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
@@ -18,7 +18,6 @@
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadata.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/ProjectScopedContainers.h"
@@ -18,7 +18,6 @@
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadata.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/ProjectScopedContainers.h"
-114
View File
@@ -407,120 +407,6 @@ void EventsRefactorer::RenameObjectInEvents(const gd::Platform& platform,
eventsParameterReplacer.Launch(events, projectScopedContainers);
}
bool EventsRefactorer::RemoveObjectInActions(const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers,
gd::InstructionsList& actions,
gd::String name) {
bool somethingModified = false;
for (std::size_t aId = 0; aId < actions.size(); ++aId) {
bool deleteMe = false;
const gd::InstructionMetadata& instrInfos =
MetadataProvider::GetActionMetadata(platform, actions[aId].GetType());
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
// Find object's name in parameters
if (gd::ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType()) &&
actions[aId].GetParameter(pNb).GetPlainString() == name) {
deleteMe = true;
break;
}
// Find object's name in expressions
else if (ParameterMetadata::IsExpression(
"number", instrInfos.parameters.GetParameter(pNb).GetType())) {
auto node = actions[aId].GetParameter(pNb).GetRootNode();
if (ExpressionObjectFinder::CheckIfHasObject(platform, projectScopedContainers, "number", *node, name)) {
deleteMe = true;
break;
}
}
// Find object's name in text expressions
else if (ParameterMetadata::IsExpression(
"string", instrInfos.parameters.GetParameter(pNb).GetType())) {
auto node = actions[aId].GetParameter(pNb).GetRootNode();
if (ExpressionObjectFinder::CheckIfHasObject(platform, projectScopedContainers, "string", *node, name)) {
deleteMe = true;
break;
}
}
}
if (deleteMe) {
somethingModified = true;
actions.Remove(aId);
aId--;
} else if (!actions[aId].GetSubInstructions().empty())
somethingModified =
RemoveObjectInActions(platform,
projectScopedContainers,
actions[aId].GetSubInstructions(),
name) ||
somethingModified;
}
return somethingModified;
}
bool EventsRefactorer::RemoveObjectInConditions(
const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers,
gd::InstructionsList& conditions,
gd::String name) {
bool somethingModified = false;
for (std::size_t cId = 0; cId < conditions.size(); ++cId) {
bool deleteMe = false;
const gd::InstructionMetadata& instrInfos =
MetadataProvider::GetConditionMetadata(platform,
conditions[cId].GetType());
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
// Find object's name in parameters
if (gd::ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType()) &&
conditions[cId].GetParameter(pNb).GetPlainString() == name) {
deleteMe = true;
break;
}
// Find object's name in expressions
else if (ParameterMetadata::IsExpression(
"number", instrInfos.parameters.GetParameter(pNb).GetType())) {
auto node = conditions[cId].GetParameter(pNb).GetRootNode();
if (ExpressionObjectFinder::CheckIfHasObject(platform, projectScopedContainers, "number", *node, name)) {
deleteMe = true;
break;
}
}
// Find object's name in text expressions
else if (ParameterMetadata::IsExpression(
"string", instrInfos.parameters.GetParameter(pNb).GetType())) {
auto node = conditions[cId].GetParameter(pNb).GetRootNode();
if (ExpressionObjectFinder::CheckIfHasObject(platform, projectScopedContainers, "string", *node, name)) {
deleteMe = true;
break;
}
}
}
if (deleteMe) {
somethingModified = true;
conditions.Remove(cId);
cId--;
} else if (!conditions[cId].GetSubInstructions().empty())
somethingModified =
RemoveObjectInConditions(platform,
projectScopedContainers,
conditions[cId].GetSubInstructions(),
name) ||
somethingModified;
}
return somethingModified;
}
gd::String ReplaceAllOccurrencesCaseInsensitive(gd::String context,
const gd::String& from,
const gd::String& to) {
-20
View File
@@ -123,26 +123,6 @@ class GD_CORE_API EventsRefactorer {
virtual ~EventsRefactorer(){};
private:
/**
* Remove all conditions of the list using an object
*
* \return true if something was modified.
*/
static bool RemoveObjectInConditions(const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers,
gd::InstructionsList& conditions,
gd::String name);
/**
* Remove all actions of the list using an object
*
* \return true if something was modified.
*/
static bool RemoveObjectInActions(const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers,
gd::InstructionsList& conditions,
gd::String name);
/**
* Replace all occurrences of a gd::String in conditions
*
@@ -18,7 +18,6 @@
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadata.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/IDE/Events/ExpressionVariableOwnerFinder.h"
#include "GDCore/IDE/Events/ExpressionVariableNameFinder.h"
#include "GDCore/IDE/VariableInstructionSwitcher.h"
@@ -18,7 +18,6 @@
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadata.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/IDE/Events/ExpressionVariableOwnerFinder.h"
#include "GDCore/IDE/Events/ExpressionVariableNameFinder.h"
#include "GDCore/IDE/VariableInstructionSwitcher.h"
@@ -580,7 +580,7 @@ class GD_CORE_API ExpressionCompletionFinder
// Complete a potential child variable:
if (variablesContainer->Has(node.identifierName)) {
AddCompletionsForChildrenVariablesOf(
&variablesContainer->Get(node.identifierName),
variablesContainer->Get(node.identifierName),
node.childIdentifierNameLocation,
node.childIdentifierName);
}
@@ -611,7 +611,7 @@ class GD_CORE_API ExpressionCompletionFinder
if (variablesContainer &&
variablesContainer->Has(node.identifierName)) {
AddCompletionsForChildrenVariablesOf(
&variablesContainer->Get(node.identifierName),
variablesContainer->Get(node.identifierName),
node.childIdentifierNameLocation,
node.childIdentifierName);
}
@@ -873,9 +873,9 @@ class GD_CORE_API ExpressionCompletionFinder
const ExpressionParserLocation& location,
gd::String eagerlyCompleteForVariableName = "") {
if (variableAndItsParent.parentVariable) {
AddCompletionsForChildrenVariablesOf(variableAndItsParent.parentVariable,
location,
eagerlyCompleteForVariableName);
AddCompletionsForChildrenVariablesOf(
*variableAndItsParent.parentVariable,
location, eagerlyCompleteForVariableName);
} else if (variableAndItsParent.parentVariablesContainer) {
AddCompletionsForVariablesMatchingSearch(
*variableAndItsParent.parentVariablesContainer, "", location);
@@ -883,16 +883,14 @@ class GD_CORE_API ExpressionCompletionFinder
}
void AddCompletionsForChildrenVariablesOf(
const gd::Variable* variable,
const gd::Variable& variable,
const ExpressionParserLocation& location,
gd::String eagerlyCompleteForVariableName = "") {
if (!variable) return;
if (variable->GetType() == gd::Variable::Structure) {
for (const auto& name : variable->GetAllChildrenNames()) {
if (variable.GetType() == gd::Variable::Structure) {
for (const auto& name : variable.GetAllChildrenNames()) {
if (!IsIdentifierSafe(name)) continue;
const auto& childVariable = variable->GetChild(name);
const auto& childVariable = variable.GetChild(name);
ExpressionCompletionDescription description(
ExpressionCompletionDescription::Variable,
location.GetStartPosition(),
@@ -902,7 +900,10 @@ class GD_CORE_API ExpressionCompletionFinder
completions.push_back(description);
if (name == eagerlyCompleteForVariableName) {
AddEagerCompletionForVariableChildren(childVariable, name, location);
AddEagerCompletionForVariableChildren(
childVariable, name,
// The scope is only displayed with the root variable.
gd::VariablesContainer::SourceType::Unknown, location);
}
}
} else {
@@ -914,6 +915,7 @@ class GD_CORE_API ExpressionCompletionFinder
void AddEagerCompletionForVariableChildren(
const gd::Variable& variable,
const gd::String& variableName,
const gd::VariablesContainer::SourceType variableScope,
const ExpressionParserLocation& location) {
if (variable.GetType() == gd::Variable::Structure) {
gd::String prefix = variableName + ".";
@@ -932,6 +934,7 @@ class GD_CORE_API ExpressionCompletionFinder
location.GetEndPosition());
description.SetCompletion(completion);
description.SetVariableType(childVariable.GetType());
description.SetVariableScope(variableScope);
completions.push_back(description);
}
}
@@ -956,7 +959,8 @@ class GD_CORE_API ExpressionCompletionFinder
if (eagerlyCompleteIfExactMatch && variableName == search) {
AddEagerCompletionForVariableChildren(
variable, variableName, location);
variable, variableName, variablesContainer.GetSourceType(),
location);
}
});
}
@@ -982,7 +986,8 @@ class GD_CORE_API ExpressionCompletionFinder
if (eagerlyCompleteIfExactMatch && variableName == search) {
AddEagerCompletionForVariableChildren(
variable, variableName, location);
variable, variableName, gd::VariablesContainer::Object,
location);
}
});
}
@@ -1032,15 +1037,17 @@ class GD_CORE_API ExpressionCompletionFinder
location.GetEndPosition());
description.SetCompletion(variableName);
description.SetVariableType(variable.GetType());
description.SetVariableScope(
auto variableScope =
projectScopedContainers.GetVariablesContainersList()
.GetVariablesContainerFromVariableOrPropertyOrParameterName(variableName)
.GetSourceType());
.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
.GetSourceType();
description.SetVariableScope(variableScope);
completions.push_back(description);
if (eagerlyCompleteIfExactMatch && variableName == search) {
AddEagerCompletionForVariableChildren(
variable, variableName, location);
variable, variableName, variableScope, location);
}
},
[&](const gd::NamedPropertyDescriptor& property) {
@@ -1084,15 +1091,17 @@ class GD_CORE_API ExpressionCompletionFinder
location.GetEndPosition());
description.SetCompletion(variableName);
description.SetVariableType(variable.GetType());
description.SetVariableScope(
auto variableScope =
projectScopedContainers.GetVariablesContainersList()
.GetVariablesContainerFromVariableOrPropertyOrParameterName(variableName)
.GetSourceType());
.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
.GetSourceType();
description.SetVariableScope(variableScope);
completions.push_back(description);
if (eagerlyCompleteIfExactMatch && variableName == search) {
AddEagerCompletionForVariableChildren(
variable, variableName, location);
variable, variableName, variableScope, location);
}
},
[&](const gd::NamedPropertyDescriptor &property) {
+80 -46
View File
@@ -66,34 +66,57 @@ size_t GetMaximumParametersNumber(
} // namespace
bool ExpressionValidator::ValidateChildVariable(
const gd::Variable &parentVariable, const gd::String &childVariableName,
const gd::ExpressionParserLocation childNameLocation,
const bool isUndeclaredVariableFatal) {
// A child variable is accessed, check it can be used in an expression.
if (!parentVariable.HasChild(childVariableName)) {
RaiseTypeError(_("No child variable with this name found."),
childNameLocation, isUndeclaredVariableFatal);
return false;
}
return true;
}
void ExpressionValidator::ValidateLastChildVariable(
const gd::Variable &lastChildVariable,
const gd::ExpressionParserLocation childNameLocation) {
const auto type = lastChildVariable.GetType();
if (parentType == Type::String || parentType == Type::Number ||
parentType == Type::NumberOrString) {
// Collections type can't be used directly in expressions, a child
// must be accessed.
if (type == Variable::Structure) {
RaiseTypeError(_("You need to specify the name of the child variable "
"to access. For example: `MyVariable.child`."),
childNameLocation);
} else if (type == Variable::Array) {
RaiseTypeError(_("You need to specify the name of the child variable "
"to access. For example: `MyVariable[0]`."),
childNameLocation);
}
}
// Number, string or boolean variables can be used in expressions.
ReadChildTypeFromVariable(type);
}
bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty(
const gd::IdentifierNode& identifier) {
return ValidateObjectVariableOrVariableOrProperty(identifier.identifierName, identifier.identifierNameLocation, identifier.childIdentifierName, identifier.childIdentifierNameLocation);
const gd::IdentifierNode &identifier) {
return ValidateObjectVariableOrVariableOrProperty(
identifier.identifierName, identifier.identifierNameLocation,
identifier.childIdentifierName, identifier.childIdentifierNameLocation,
true);
}
bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty(
const gd::String &identifierName,
const gd::ExpressionParserLocation identifierNameLocation,
const gd::String &childIdentifierName,
const gd::ExpressionParserLocation childIdentifierNameLocation) {
auto validateVariableTypeForExpression =
[this, &identifierNameLocation](gd::Variable::Type type) {
// Collections type can't be used directly in expressions, a child
// must be accessed.
if (type == Variable::Structure) {
RaiseTypeError(_("You need to specify the name of the child variable "
"to access. For example: `MyVariable.child`."),
identifierNameLocation);
} else if (type == Variable::Array) {
RaiseTypeError(_("You need to specify the name of the child variable "
"to access. For example: `MyVariable[0]`."),
identifierNameLocation);
} else {
// Number, string or boolean variables can be used in expressions.
return;
}
};
const gd::ExpressionParserLocation childIdentifierNameLocation,
const bool isUndeclaredVariableFatal,
const bool hasMoreChildren) {
const auto& variablesContainersList = projectScopedContainers.GetVariablesContainersList();
const auto& objectsContainersList = projectScopedContainers.GetObjectsContainersList();
const auto& propertiesContainersList = projectScopedContainers.GetPropertiesContainersList();
@@ -118,27 +141,34 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty(
identifierName, childIdentifierName);
if (variableExistence == gd::ObjectsContainersList::DoesNotExist) {
RaiseUndeclaredVariableError(_("This variable does not exist on this object or group."),
childIdentifierNameLocation, childIdentifierName, identifierName);
RaiseUndeclaredVariableError(
_("This variable does not exist on this object or group."),
childIdentifierNameLocation, childIdentifierName, identifierName,
isUndeclaredVariableFatal);
return true; // We should have found a variable.
}
else if (variableExistence == gd::ObjectsContainersList::ExistsOnlyOnSomeObjectsOfTheGroup) {
RaiseUndeclaredVariableError(_("This variable only exists on some objects of the group. It must be declared for all objects."),
childIdentifierNameLocation, childIdentifierName, identifierName);
} else if (variableExistence ==
gd::ObjectsContainersList::ExistsOnlyOnSomeObjectsOfTheGroup) {
RaiseUndeclaredVariableError(
_("This variable only exists on some objects of the group. It must be declared for all objects."),
childIdentifierNameLocation, childIdentifierName, identifierName,
isUndeclaredVariableFatal);
return true; // We should have found a variable.
}
else if (variableExistence == gd::ObjectsContainersList::GroupIsEmpty) {
} else if (variableExistence == gd::ObjectsContainersList::GroupIsEmpty) {
RaiseUndeclaredVariableError(_("This group is empty. Add an object to this group first."),
identifierNameLocation, childIdentifierName, identifierName);
return true; // We should have found a variable.
}
auto variableType = objectsContainersList.GetTypeOfObjectOrGroupVariable(
identifierName, childIdentifierName);
ReadChildTypeFromVariable(variableType);
if (!hasMoreChildren) {
const auto &objectVariable =
objectsContainersList
.GetObjectOrGroupVariablesContainer(identifierName)
->Get(childIdentifierName);
ValidateLastChildVariable(objectVariable, childIdentifierNameLocation);
}
return true; // We found a variable.
}, [&]() {
@@ -152,22 +182,16 @@ bool ExpressionValidator::ValidateObjectVariableOrVariableOrProperty(
if (childIdentifierName.empty()) {
// Just the root variable is accessed, check it can be used in an
// expression.
validateVariableTypeForExpression(variable.GetType());
ReadChildTypeFromVariable(variable.GetType());
ValidateLastChildVariable(variable, identifierNameLocation);
return true; // We found a variable.
} else {
// A child variable is accessed, check it can be used in an expression.
if (!variable.HasChild(childIdentifierName)) {
RaiseTypeError(_("No child variable with this name found."),
childIdentifierNameLocation);
return true; // We should have found a variable.
ValidateChildVariable(variable, childIdentifierName,
childIdentifierNameLocation,
isUndeclaredVariableFatal);
if (variable.HasChild(childIdentifierName)) {
ValidateLastChildVariable(variable.GetChild(childIdentifierName),
childIdentifierNameLocation);
}
const gd::Variable& childVariable =
variable.GetChild(childIdentifierName);
ReadChildTypeFromVariable(childVariable.GetType());
return true; // We found a variable.
}
}, [&]() {
@@ -388,6 +412,12 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction(
return returnType;
}
const gd::String parentRootObjectName = rootObjectName;
// We don't check objectvar parameters since they are only used by the legacy
// functions like: `Object.Variable(MyVariable)` which allow undeclared
// variables.
rootObjectName = emptyParameterExtraInfo;
// TODO: reverse the order of diagnostic?
size_t writtenParametersFirstIndex =
ExpressionParser2::WrittenParametersFirstIndex(function.objectName,
@@ -448,6 +478,7 @@ ExpressionValidator::Type ExpressionValidator::ValidateFunction(
}
metadataIndex++;
}
rootObjectName = parentRootObjectName;
return returnType;
}
@@ -478,6 +509,7 @@ const gd::String& ExpressionValidator::TypeToString(Type type) {
// if it allows properties and parameters.
case Type::VariableOrProperty:
case Type::VariableOrPropertyOrParameter:
case Type::ObjectVariable:
case Type::LegacyVariable:
return variableTypeString;
case Type::Object:
@@ -506,7 +538,9 @@ ExpressionValidator::Type ExpressionValidator::StringToType(
if (type == ExpressionValidator::variableTypeString ||
gd::ParameterMetadata::IsExpression(
ExpressionValidator::variableTypeString, type)) {
if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) {
if (gd::ValueTypeMetadata::IsTypeObjectVariable(type)) {
return Type::ObjectVariable;
} else if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) {
return Type::LegacyVariable;
} else if (type == "variableOrProperty") {
return Type::VariableOrProperty;
+189 -24
View File
@@ -40,10 +40,12 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
ExpressionValidator(const gd::Platform &platform_,
const gd::ProjectScopedContainers & projectScopedContainers_,
const gd::String &rootType_,
const gd::String &rootObjectName_ = emptyParameterExtraInfo,
const gd::String &extraInfo_ = emptyParameterExtraInfo)
: platform(platform_),
projectScopedContainers(projectScopedContainers_),
parentType(StringToType(gd::ValueTypeMetadata::GetExpressionPrimitiveValueType(rootType_))),
rootObjectName(rootObjectName_),
childType(Type::Unknown),
forbidsUsageOfBracketsBecauseParentIsObject(false),
currentParameterExtraInfo(&extraInfo_),
@@ -103,14 +105,19 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
node.leftHandSide->Visit(*this);
const Type leftType = childType; // Store the type of the first operand.
if (leftType == Type::Number) {
if (parentType == Type::Variable || parentType == Type::ObjectVariable ||
parentType == Type::LegacyVariable) {
RaiseOperatorError(
_("Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name."),
node.rightHandSide->location);
} else if (leftType == Type::Number) {
if (node.op == ' ') {
RaiseError(gd::ExpressionParserError::ErrorType::SyntaxError,
"No operator found. Did you forget to enter an operator (like +, -, "
"* or /) between numbers or expressions?", node.rightHandSide->location);
}
}
else if (leftType == Type::String) {
} else if (leftType == Type::String) {
if (node.op == ' ') {
RaiseError(gd::ExpressionParserError::ErrorType::SyntaxError,
"You must add the operator + between texts or expressions. For "
@@ -127,11 +134,6 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
_("Operators (+, -, /, *) can't be used with an object name. Remove "
"the operator."),
node.rightHandSide->location);
} else if (leftType == Type::Variable || leftType == Type::LegacyVariable) {
RaiseOperatorError(
_("Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name."),
node.rightHandSide->location);
}
// The "required" type ("parentType") of the second operator is decided by:
@@ -153,7 +155,13 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
node.factor->Visit(*this);
const Type rightType = childType;
if (rightType == Type::Number) {
if (parentType == Type::Variable || parentType == Type::ObjectVariable ||
parentType == Type::LegacyVariable) {
RaiseTypeError(
_("Operators (+, -) can't be used in variable names. Remove "
"the operator from the variable name."),
node.location);
} else if (rightType == Type::Number) {
if (node.op != '+' && node.op != '-') {
// This is actually a dead code because the parser takes them as
// binary operations with an empty left side which makes as much sense.
@@ -174,11 +182,6 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
_("Operators (+, -) can't be used with an object name. Remove the "
"operator."),
node.location);
} else if (rightType == Type::Variable || rightType == Type::LegacyVariable) {
RaiseTypeError(
_("Operators (+, -) can't be used in variable names. Remove "
"the operator from the variable name."),
node.location);
}
}
void OnVisitNumberNode(NumberNode& node) override {
@@ -188,6 +191,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
RaiseTypeError(
_("You entered a number, but a text was expected (in quotes)."),
node.location);
} else if (parentType == Type::Variable ||
parentType == Type::ObjectVariable ||
parentType == Type::LegacyVariable) {
RaiseTypeError(
_("The variable name looks like you're building an expression or a "
"formula. You can only use this for structure or arrays, for "
"example: Score[3]."),
node.location);
} else if (parentType != Type::Number &&
parentType != Type::NumberOrString) {
RaiseTypeError(_("You entered a number, but this type was expected:") +
@@ -201,6 +212,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
if (parentType == Type::Number) {
RaiseTypeError(_("You entered a text, but a number was expected."),
node.location);
} else if (parentType == Type::Variable ||
parentType == Type::ObjectVariable ||
parentType == Type::LegacyVariable) {
RaiseTypeError(
_("The variable name looks like you're building an expression or a "
"formula. You can only use this for structure or arrays, for "
"example: Score[\"Player1\"]."),
node.location);
} else if (parentType != Type::String &&
parentType != Type::NumberOrString) {
RaiseTypeError(_("You entered a text, but this type was expected:") +
@@ -210,13 +229,51 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
}
void OnVisitVariableNode(VariableNode& node) override {
ReportAnyError(node);
parentVariable = nullptr;
variableChildDepth = 0;
if (parentType == Type::Variable ||
parentType == Type::VariableOrProperty ||
parentType == Type::VariableOrPropertyOrParameter) {
childType = parentType;
CheckVariableExistence(node.location, node.name, node.child != nullptr);
bool isRootVariableDeclared = CheckVariableExistence(
node.location, node.name, node.child != nullptr);
if (node.child) {
if (isRootVariableDeclared) {
const auto &variable =
projectScopedContainers.GetVariablesContainersList().Get(
node.name);
parentVariable = &variable;
}
node.child->Visit(*this);
}
} else if (parentType == Type::ObjectVariable) {
childType = parentType;
if (!rootObjectName.empty()) {
ValidateObjectVariableOrVariableOrProperty(
rootObjectName, node.nameLocation, node.name, node.nameLocation,
false, !!node.child);
const auto &objectsContainersList =
projectScopedContainers.GetObjectsContainersList();
auto variableExistence =
objectsContainersList.HasObjectOrGroupWithVariableNamed(
rootObjectName, node.name);
if (variableExistence == gd::ObjectsContainersList::Exists) {
const auto &objectVariable =
objectsContainersList
.GetObjectOrGroupVariablesContainer(rootObjectName)
->Get(node.name);
if (node.child) {
parentVariable = &objectVariable;
} else {
ValidateLastChildVariable(objectVariable, node.nameLocation);
}
}
rootObjectName = "";
}
if (node.child) {
node.child->Visit(*this);
}
@@ -246,6 +303,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
forbidsUsageOfBracketsBecauseParentIsObject = true;
}, [&]() {
// This is a variable.
const auto &variable =
projectScopedContainers.GetVariablesContainersList().Get(
node.name);
if (node.child) {
parentVariable = &variable;
} else {
ValidateLastChildVariable(variable, node.location);
}
}, [&]() {
// This is a property.
// Being in this node implies that there is at least a child - which is not supported for properties.
@@ -281,10 +346,48 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
ReportAnyError(node);
// TODO Also check child-variables existence on a path with only VariableAccessor to raise non-fatal errors.
if (!variableObjectName.empty()) {
ValidateObjectVariableOrVariableOrProperty(variableObjectName,
variableObjectNameLocation,
node.name, node.nameLocation);
ValidateObjectVariableOrVariableOrProperty(
variableObjectName, variableObjectNameLocation, node.name,
node.nameLocation, true, !!node.child);
const auto &objectsContainersList =
projectScopedContainers.GetObjectsContainersList();
auto variableExistence =
objectsContainersList.HasObjectOrGroupWithVariableNamed(
variableObjectName, node.name);
if (variableExistence == gd::ObjectsContainersList::Exists) {
const auto &objectVariable =
objectsContainersList
.GetObjectOrGroupVariablesContainer(variableObjectName)
->Get(node.name);
if (node.child) {
parentVariable = &objectVariable;
} else {
parentVariable = nullptr;
}
} else {
parentVariable = nullptr;
}
variableChildDepth = 0;
variableObjectName = "";
} else if (parentVariable) {
const bool isChildVariableDeclared = ValidateChildVariable(
*parentVariable, node.name, node.nameLocation, false);
if (isChildVariableDeclared) {
const auto &childVariable = parentVariable->GetChild(node.name);
if (node.child) {
parentVariable = &childVariable;
variableChildDepth++;
} else {
ValidateLastChildVariable(childVariable, node.nameLocation);
parentVariable = nullptr;
variableChildDepth = 0;
}
}
else {
parentVariable = nullptr;
variableChildDepth = 0;
}
}
// In the case we accessed an object variable (`MyObject.MyVariable`),
// brackets can now be used (`MyObject.MyVariable["MyChildVariable"]` is now valid).
@@ -299,6 +402,8 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
ReportAnyError(node);
variableObjectName = "";
parentVariable = nullptr;
variableChildDepth = 0;
if (forbidsUsageOfBracketsBecauseParentIsObject) {
RaiseError(gd::ExpressionParserError::ErrorType::BracketsNotAllowedForObjects,
_("You can't use the brackets to access an object variable. "
@@ -350,7 +455,46 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
} else if (parentType == Type::Variable ||
parentType == Type::VariableOrProperty ||
parentType == Type::VariableOrPropertyOrParameter) {
CheckVariableExistence(node.location, node.identifierName, !node.childIdentifierName.empty());
bool isRootVariableDeclared =
CheckVariableExistence(node.location, node.identifierName,
!node.childIdentifierName.empty());
if (isRootVariableDeclared && !node.childIdentifierName.empty()) {
ValidateObjectVariableOrVariableOrProperty(
node.identifierName, node.identifierNameLocation,
node.childIdentifierName, node.childIdentifierNameLocation, false);
}
} else if (parentType == Type::ObjectVariable) {
childType = parentType;
if (!rootObjectName.empty()) {
ValidateObjectVariableOrVariableOrProperty(
rootObjectName, node.identifierNameLocation, node.identifierName,
node.identifierNameLocation, false,
!node.childIdentifierName.empty());
const auto &objectsContainersList =
projectScopedContainers.GetObjectsContainersList();
auto variableExistence =
objectsContainersList.HasObjectOrGroupWithVariableNamed(
rootObjectName, node.identifierName);
if (variableExistence == gd::ObjectsContainersList::Exists) {
const auto &objectVariable =
objectsContainersList
.GetObjectOrGroupVariablesContainer(rootObjectName)
->Get(node.identifierName);
if (!node.childIdentifierName.empty()) {
const bool isChildVariableDeclared =
ValidateChildVariable(objectVariable, node.childIdentifierName,
node.childIdentifierNameLocation, false);
if (isChildVariableDeclared) {
const auto &childVariable =
objectVariable.GetChild(node.childIdentifierName);
ValidateLastChildVariable(childVariable,
node.childIdentifierNameLocation);
}
}
}
rootObjectName = "";
}
} else if (parentType != Type::Object &&
parentType != Type::LegacyVariable) {
// It can't happen.
@@ -376,7 +520,9 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
} else if (parentType == Type::String) {
message = _(
"You must enter a text (between quotes) or a valid expression call.");
} else if (parentType == Type::Variable || parentType == Type::LegacyVariable) {
} else if (parentType == Type::Variable ||
parentType == Type::ObjectVariable ||
parentType == Type::LegacyVariable) {
message = _("You must enter a variable name.");
} else if (parentType == Type::Object) {
message = _("You must enter a valid object name.");
@@ -395,6 +541,7 @@ private:
String,
NumberOrString,
Variable,
ObjectVariable,
LegacyVariable,
Object,
Empty,
@@ -407,12 +554,22 @@ private:
const gd::String &identifierName,
const gd::ExpressionParserLocation identifierNameLocation,
const gd::String &childIdentifierName,
const gd::ExpressionParserLocation childIdentifierNameLocation);
const gd::ExpressionParserLocation childIdentifierNameLocation,
const bool isUndeclaredVariableFatal,
const bool hasMoreChildren = false);
bool ValidateChildVariable(
const gd::Variable &parentVariable, const gd::String &childVariableName,
const gd::ExpressionParserLocation childNameLocation,
const bool isUndeclaredVariableFatal);
void ValidateLastChildVariable(
const gd::Variable &lastChildVariable,
const gd::ExpressionParserLocation childNameLocation);
void CheckVariableExistence(const ExpressionParserLocation &location,
bool CheckVariableExistence(const ExpressionParserLocation &location,
const gd::String &name, bool hasChild) {
if (!currentParameterExtraInfo ||
*currentParameterExtraInfo != "AllowUndeclaredVariable") {
bool isRootVariableDeclared = false;
projectScopedContainers.MatchIdentifierWithName<void>(
name,
[&]() {
@@ -424,6 +581,7 @@ private:
},
[&]() {
// This is a variable.
isRootVariableDeclared = true;
},
[&]() {
// This is a property.
@@ -456,7 +614,9 @@ private:
_("No variable with this name found."), location,
name);
});
return isRootVariableDeclared;
}
return false;
}
void ReportAnyError(const ExpressionNode& node, bool isFatal = true) {
@@ -497,9 +657,10 @@ private:
void RaiseUndeclaredVariableError(const gd::String &message,
const ExpressionParserLocation &location,
const gd::String &variableName,
const gd::String &objectName = "") {
const gd::String &objectName = "",
const bool isUndeclaredVariableFatal = true) {
RaiseError(gd::ExpressionParserError::ErrorType::UndeclaredVariable,
message, location, true, variableName, objectName);
message, location, isUndeclaredVariableFatal, variableName, objectName);
}
void RaiseVariableNameCollisionError(const gd::String &message,
@@ -566,9 +727,13 @@ private:
std::vector<std::unique_ptr<ExpressionParserError>> supplementalErrors;
Type childType; ///< The type "discovered" down the tree and passed up.
Type parentType; ///< The type "required" by the top of the tree.
/** The root object name of the expression or a function call. */
gd::String rootObjectName;
bool forbidsUsageOfBracketsBecauseParentIsObject;
gd::String variableObjectName;
gd::ExpressionParserLocation variableObjectNameLocation;
const gd::Variable *parentVariable = nullptr;
size_t variableChildDepth = 0;
const gd::String *currentParameterExtraInfo;
const gd::Platform &platform;
const gd::ProjectScopedContainers &projectScopedContainers;
@@ -14,7 +14,6 @@
#include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h"
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
@@ -14,7 +14,6 @@
#include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h"
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
@@ -15,7 +15,6 @@
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
@@ -0,0 +1,48 @@
/*
* GDevelop Core
* Copyright 2008-present Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#include "EventsFunctionsExtensionBrowser.h"
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
#include "GDCore/IDE/Project/ArbitraryEventBasedBehaviorsWorker.h"
#include "GDCore/IDE/Project/ArbitraryEventsFunctionsWorker.h"
#include "GDCore/IDE/ProjectBrowserHelper.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
namespace gd {
void EventsFunctionsExtensionBrowser::ExposeEvents(
gd::Project &project, gd::ArbitraryEventsWorker &worker) const {
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
project, eventsFunctionsExtension, worker);
}
void EventsFunctionsExtensionBrowser::ExposeEvents(
gd::Project &project, gd::ArbitraryEventsWorkerWithContext &worker) const {
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
project, eventsFunctionsExtension, worker);
}
void EventsFunctionsExtensionBrowser::ExposeFunctions(
gd::Project &project, gd::ArbitraryEventsFunctionsWorker &worker) const {
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionFunctions(
project, eventsFunctionsExtension, worker);
}
void EventsFunctionsExtensionBrowser::ExposeEventBasedBehaviors(
gd::Project &project,
gd::ArbitraryEventBasedBehaviorsWorker &worker) const {
worker.Launch(eventsFunctionsExtension.GetEventsBasedBehaviors());
}
void EventsFunctionsExtensionBrowser::ExposeObjects(
gd::Project &project, gd::ArbitraryObjectsWorker &worker) const {
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionObjects(
eventsFunctionsExtension, worker);
}
} // namespace gd
@@ -0,0 +1,97 @@
/*
* GDevelop Core
* Copyright 2008-present Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/IDE/ProjectBrowser.h"
namespace gd {
class Project;
class String;
class EventsFunctionsExtension;
class EventsFunction;
class EventsBasedBehavior;
class EventsBasedObject;
class ArbitraryEventsWorker;
class ArbitraryEventsWorkerWithContext;
class ArbitraryEventsFunctionsWorker;
class ArbitraryObjectsWorker;
class ArbitraryEventBasedBehaviorsWorker;
class ArbitraryBehaviorSharedDataWorker;
} // namespace gd
namespace gd {
/**
* \brief Expose events-functions extension contents to workers.
*/
class GD_CORE_API EventsFunctionsExtensionBrowser : public ProjectBrowser {
public:
EventsFunctionsExtensionBrowser(
gd::EventsFunctionsExtension &eventsFunctionsExtension_)
: eventsFunctionsExtension(eventsFunctionsExtension_) {}
/**
* \brief Call the specified worker on all events of the events-functions
* extension.
*
* This should be the preferred way to traverse all the events of an
* events-functions extension.
*/
void ExposeEvents(gd::Project &project,
gd::ArbitraryEventsWorker &worker) const override;
/**
* \brief Call the specified worker on all events of the events-functions
* extension.
*
* This should be the preferred way to traverse all the events of an
* events-functions extension.
*/
void
ExposeEvents(gd::Project &project,
gd::ArbitraryEventsWorkerWithContext &worker) const override;
/**
* \brief Call the specified worker on all functions of the events-functions
* extension (free function, behavior function and object function).
*
* This should be the preferred way to traverse all the function signatures
* of an events-functions extension.
*/
void
ExposeFunctions(gd::Project &project,
gd::ArbitraryEventsFunctionsWorker &worker) const override;
/**
* \brief Call the specified worker on all ObjectContainers of the
* events-functions extension.
*
* This should be the preferred way to traverse all the objects of an
* events-functions extension.
*/
void ExposeObjects(gd::Project &project,
gd::ArbitraryObjectsWorker &worker) const override;
/**
* \brief Call the specified worker on the event-based behaviors of the
* events-functions extension.
*/
void ExposeEventBasedBehaviors(
gd::Project &project,
gd::ArbitraryEventBasedBehaviorsWorker &worker) const override;
/**
* \brief Do nothing.
*/
void ExposeBehaviorSharedDatas(
gd::Project &project,
gd::ArbitraryBehaviorSharedDataWorker &worker) const override {};
private:
gd::EventsFunctionsExtension &eventsFunctionsExtension;
};
} // namespace gd
@@ -0,0 +1,248 @@
/*
* GDevelop Core
* Copyright 2008-present Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#include "ChildObjectForwardFunctionGenerator.h"
#include "GDCore/Events/Builtin/CommentEvent.h"
#include "GDCore/Events/Builtin/StandardEvent.h"
#include "GDCore/Extensions/Metadata/BehaviorMetadata.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/WholeProjectRefactorer.h"
#include "GDCore/Project/EventsBasedObject.h"
#include "GDCore/Project/EventsFunction.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
namespace gd {
void ChildObjectForwardFunctionGenerator::GenerateChildObjectForwardFunctions(
const gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
gd::EventsBasedObject &eventsBasedObject,
const gd::String &childObjectName) {
if (!eventsBasedObject.GetObjects().HasObjectNamed(childObjectName)) {
return;
}
auto &childObject = eventsBasedObject.GetObjects().GetObject(childObjectName);
if (!project.HasEventsBasedObject(childObject.GetType())) {
return;
}
auto &childEventsBasedObject =
project.GetEventsBasedObject(childObject.GetType());
for (auto &childEventsFunction :
childEventsBasedObject.GetEventsFunctions().GetInternalVector()) {
if (childEventsFunction->IsPrivate() ||
childEventsFunction->IsDeprecated() ||
gd::EventsBasedObject::IsObjectLifecycleEventsFunction(
childEventsFunction->GetName())) {
continue;
}
ChildObjectForwardFunctionGenerator::GenerateChildObjectForwardFunction(
project, eventsFunctionsExtension, eventsBasedObject, childObjectName,
*childEventsFunction);
}
}
void ChildObjectForwardFunctionGenerator::GenerateChildObjectForwardFunction(
const gd::Project &project,
const gd::EventsFunctionsExtension &parentEventsFunctionsExtension,
gd::EventsBasedObject &parentEventsBasedObject,
const gd::String &childObjectName,
const gd::EventsFunction &childEventsFunction) {
if (!parentEventsBasedObject.GetObjects().HasObjectNamed(childObjectName)) {
return;
}
auto &childObject =
parentEventsBasedObject.GetObjects().GetObject(childObjectName);
if (!project.HasEventsBasedObject(childObject.GetType())) {
return;
}
auto &childEventsBasedObject =
project.GetEventsBasedObject(childObject.GetType());
auto &functionName = childEventsFunction.GetName();
if (parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
functionName)) {
return;
}
auto &parentEventsFunction =
parentEventsBasedObject.GetEventsFunctions().InsertEventsFunction(
childEventsFunction, 0);
parentEventsFunction.GetEvents().Clear();
parentEventsBasedObject.GetEventsFunctions()
.AddMissingFunctionsInRootFolder();
auto &rootFolder =
parentEventsBasedObject.GetEventsFunctions().GetRootFolder();
// TODO Handle sub-folders
auto &folder =
!childEventsFunction.GetGroup().empty()
? rootFolder.GetOrCreateChildFolder(childEventsFunction.GetGroup())
: rootFolder;
rootFolder.MoveFunctionFolderOrFunctionToAnotherFolder(
rootFolder.GetFunctionNamed(parentEventsFunction.GetName()), folder,
folder.GetChildrenCount());
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
parentEventsFunctionsExtension, parentEventsBasedObject);
auto childExtensionName =
gd::PlatformExtension::GetExtensionFromFullObjectType(
childObject.GetType());
auto childFunctionType =
gd::PlatformExtension::GetObjectEventsFunctionFullType(
childExtensionName, childEventsBasedObject.GetName(), functionName);
bool hasAnyBooleanParameter = false;
for (auto &parameter :
parentEventsFunction.GetParameters().GetInternalVector()) {
if (parameter->GetValueTypeMetadata().IsBoolean()) {
hasAnyBooleanParameter = true;
break;
}
}
if (hasAnyBooleanParameter &&
parentEventsFunction.GetParameters().GetParametersCount() > 2) {
auto &event = dynamic_cast<gd::CommentEvent &>(
parentEventsFunction.GetEvents().InsertNewEvent(
project, "BuiltinCommonInstructions::Comment", 0));
event.SetComment("TODO: Please implement this function manually.");
return;
}
auto &event = dynamic_cast<gd::StandardEvent &>(
parentEventsFunction.GetEvents().InsertNewEvent(
project, "BuiltinCommonInstructions::Standard", 0));
switch (childEventsFunction.GetFunctionType()) {
case gd::EventsFunction::FunctionType::ActionWithOperator:
case gd::EventsFunction::FunctionType::Action: {
if (parentEventsFunction.GetParameters().GetParametersCount() == 2 &&
hasAnyBooleanParameter) {
gd::Instruction condition;
condition.SetType("BooleanVariable");
condition.AddParameter("Value");
condition.AddParameter("True");
condition.AddParameter("");
event.GetConditions().Insert(condition, 0);
gd::Instruction action;
action.SetType(childFunctionType);
action.AddParameter(childObjectName);
action.AddParameter("yes");
event.GetActions().Insert(action, 0);
{
auto &event = dynamic_cast<gd::StandardEvent &>(
parentEventsFunction.GetEvents().InsertNewEvent(
project, "BuiltinCommonInstructions::Standard", 0));
gd::Instruction condition;
condition.SetType("BooleanVariable");
condition.AddParameter("Value");
condition.AddParameter("False");
condition.AddParameter("");
event.GetConditions().Insert(condition, 0);
gd::Instruction action;
action.SetType(childFunctionType);
action.AddParameter(childObjectName);
action.AddParameter("no");
event.GetActions().Insert(action, 0);
}
} else {
gd::Instruction action;
action.SetType(childFunctionType);
for (auto &parameter :
childEventsFunction
.GetParametersForEvents(
parentEventsBasedObject.GetEventsFunctions())
.GetInternalVector()) {
if (childEventsFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::ActionWithOperator &&
parameter->GetName() == "Value") {
action.AddParameter("=");
}
action.AddParameter(parameter->GetName());
}
action.SetParameter(0, childObjectName);
event.GetActions().Insert(action, 0);
if (childEventsFunction.IsAsync()) {
gd::Instruction action;
action.SetType("BuiltinAsync::ResolveAsyncEventsFunction");
event.GetActions().Insert(action, 1);
}
}
break;
}
case gd::EventsFunction::FunctionType::Condition: {
gd::Instruction condition;
condition.SetType(childFunctionType);
for (auto &parameter : childEventsFunction
.GetParametersForEvents(
parentEventsBasedObject.GetEventsFunctions())
.GetInternalVector()) {
condition.AddParameter(parameter->GetName());
}
condition.SetParameter(0, childObjectName);
event.GetConditions().Insert(condition, 0);
gd::Instruction action;
action.SetType("SetReturnBoolean");
action.AddParameter("True");
event.GetActions().Insert(action, 0);
if (childEventsFunction.IsAsync()) {
gd::Instruction action;
action.SetType("BuiltinAsync::ResolveAsyncEventsFunction");
event.GetActions().Insert(action, 1);
}
break;
}
case gd::EventsFunction::FunctionType::ExpressionAndCondition:
case gd::EventsFunction::FunctionType::Expression: {
gd::Instruction action;
action.SetType(childEventsFunction.GetExpressionType().IsNumber()
? "SetReturnNumber"
: "SetReturnString");
gd::String expression =
childObjectName + "." + childEventsFunction.GetName() + "(";
auto &parameters = childEventsFunction.GetParametersForEvents(
parentEventsBasedObject.GetEventsFunctions());
for (size_t i = 1; i < parameters.GetParametersCount(); i++) {
if (i > 1) {
expression += ", ";
}
expression += parameters.GetParameter(i).GetName();
}
expression += ")";
action.AddParameter(expression);
event.GetActions().Insert(action, 0);
break;
}
default:
break;
}
}
bool ChildObjectForwardFunctionGenerator::HasAnyChildCustomObject(
const gd::Project &project, gd::EventsBasedObject &eventsBasedObject) {
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
if (project.HasEventsBasedObject(childObject->GetType())) {
return true;
}
}
return false;
}
std::vector<gd::String>
ChildObjectForwardFunctionGenerator::GetChildCustomObjectNames(
const gd::Project &project, gd::EventsBasedObject &eventsBasedObject) {
std::vector<gd::String> objectNames;
for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
if (project.HasEventsBasedObject(childObject->GetType())) {
objectNames.push_back(childObject->GetName());
}
}
return objectNames;
}
} // namespace gd
@@ -0,0 +1,47 @@
/*
* GDevelop Core
* Copyright 2008-present Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/String.h"
#include <vector>
namespace gd {
class Project;
class EventsFunctionsExtension;
class EventsBasedObject;
class EventsFunction;
class Object;
} // namespace gd
namespace gd {
/**
* Generate events functions in a custom object that forwards the call to one of
* its child object. It allows users to quickly expose features from a child
* object.
*/
class GD_CORE_API ChildObjectForwardFunctionGenerator {
public:
static void GenerateChildObjectForwardFunctions(
const gd::Project &project,
const gd::EventsFunctionsExtension &parentEventsFunctionsExtension,
gd::EventsBasedObject &parentEventsBasedObject,
const gd::String &childObjectName);
static void GenerateChildObjectForwardFunction(
const gd::Project &project,
const gd::EventsFunctionsExtension &parentEventsFunctionsExtension,
gd::EventsBasedObject &parentEventsBasedObject,
const gd::String &childObjectName,
const gd::EventsFunction &eventsFunction);
static bool HasAnyChildCustomObject(const gd::Project &project,
gd::EventsBasedObject &eventsBasedObject);
static std::vector<gd::String>
GetChildCustomObjectNames(const gd::Project &project,
gd::EventsBasedObject &eventsBasedObject);
};
} // namespace gd
+33 -21
View File
@@ -65,10 +65,14 @@ ParameterValidationResult InstructionValidator::ValidateParameter(
if (gd::ParameterMetadata::IsExpression("number", parameterType) ||
gd::ParameterMetadata::IsExpression("string", parameterType) ||
gd::ParameterMetadata::IsExpression("variable", parameterType)) {
gd::String rootObjectName = InstructionValidator::GetObjectNameForParameter(
projectScopedContainers, instruction, parameterType);
auto &expressionNode =
*instruction.GetParameter(parameterIndex).GetRootNode();
ExpressionValidator expressionValidator(platform, projectScopedContainers,
parameterType,
rootObjectName,
parameterMetadata.GetExtraInfo());
expressionNode.Visit(expressionValidator);
@@ -78,27 +82,6 @@ ParameterValidationResult InstructionValidator::ValidateParameter(
if (!expressionValidator.GetDeprecationWarnings().empty()) {
result.hasDeprecationWarning = true;
}
// New object variable instructions require the variable to be
// declared while legacy ones don't.
// This is why it's done here instead of in the parser directly.
if (result.isValid && parameterType == "objectvar" &&
gd::VariableInstructionSwitcher::IsSwitchableVariableInstruction(
instruction.GetType())) {
// Check at least the name of the root variable, it's the best we can
// do.
const auto &objectsContainersList =
projectScopedContainers.GetObjectsContainersList();
const auto &objectName = instruction.GetParameter(0).GetPlainString();
const auto &variableName =
instruction.GetParameter(parameterIndex).GetPlainString();
if (objectsContainersList.HasObjectOrGroupWithVariableNamed(
objectName,
gd::InstructionValidator::GetRootVariableName(variableName)) ==
gd::ObjectsContainersList::DoesNotExist) {
result.isValid = false;
}
}
} else if (gd::ParameterMetadata::IsObject(parameterType)) {
const auto &objectOrGroupName =
instruction.GetParameter(parameterIndex).GetPlainString();
@@ -121,6 +104,35 @@ ParameterValidationResult InstructionValidator::ValidateParameter(
return result;
}
gd::String InstructionValidator::GetObjectNameForParameter(
const gd::ProjectScopedContainers projectScopedContainers,
const gd::Instruction &instruction, const gd::String &parameterType) {
// New object variable instructions require the variable to be
// declared while legacy ones don't.
// For legacy variable instruction, we pass an empty object name.
gd::String rootObjectName = "";
if (parameterType != "objectvar") {
return "";
}
if (instruction.GetParametersCount() == 0) {
return "";
}
rootObjectName = instruction.GetParameter(0).GetPlainString();
if (!gd::VariableInstructionSwitcher::IsSwitchableVariableInstruction(
instruction.GetType())) {
// Extensions still rely on legacy object variables instructions.
auto objectSourceType =
projectScopedContainers.GetObjectsContainersList()
.GetObjectsContainerSourceType(rootObjectName);
// Only child-object variable declarations are checked.
if (objectSourceType != gd::ObjectsContainer::SourceType::Object) {
return "";
}
}
return rootObjectName;
}
bool InstructionValidator::IsParameterValid(
const gd::Platform &platform,
const gd::ProjectScopedContainers projectScopedContainers,
+4
View File
@@ -63,6 +63,10 @@ public:
static gd::String GetRootVariableName(const gd::String &name);
static gd::String GetObjectNameForParameter(
const gd::ProjectScopedContainers projectScopedContainers,
const gd::Instruction &instruction, const gd::String &parameterType);
private:
static bool
HasRequiredBehaviors(const gd::Instruction &instruction,
+15
View File
@@ -451,6 +451,21 @@ void ProjectBrowserHelper::ExposeEventsBasedObjectEvents(
propertyVariablesContainer, worker);
}
void ProjectBrowserHelper::ExposeEventsFunctionsExtensionFunctions(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
ArbitraryEventsFunctionsWorker &worker) {
worker.Launch(eventsFunctionsExtension.GetEventsFunctions());
for (auto &&eventsBasedBehavior :
eventsFunctionsExtension.GetEventsBasedBehaviors().GetInternalVector()) {
worker.Launch(eventsBasedBehavior->GetEventsFunctions());
}
for (auto &&eventsBasedObject :
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
worker.Launch(eventsBasedObject->GetEventsFunctions());
}
}
void ProjectBrowserHelper::ExposeProjectObjects(
gd::Project &project, gd::ArbitraryObjectsWorker &worker) {
+12
View File
@@ -239,6 +239,18 @@ public:
const gd::EventsBasedObject &eventsBasedObject,
gd::ReadOnlyArbitraryEventsWorkerWithContext &worker);
/**
* \brief Call the specified worker on all functions of the event-based
* extension.
*
* This should be the preferred way to traverse all the functions of an events
* based extension.
*/
static void ExposeEventsFunctionsExtensionFunctions(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
ArbitraryEventsFunctionsWorker &worker);
/**
* \brief Call the specified worker on all ObjectContainers of the project
* (global, layouts...)
+114 -251
View File
@@ -14,6 +14,7 @@
#include "GDCore/IDE/ObjectRefactorer.h"
#include "GDCore/IDE/EventBasedBehaviorBrowser.h"
#include "GDCore/IDE/EventBasedObjectBrowser.h"
#include "GDCore/IDE/EventsFunctionsExtensionBrowser.h"
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
#include "GDCore/IDE/Events/BehaviorParametersFiller.h"
#include "GDCore/IDE/Events/BehaviorPropertyRenamer.h"
@@ -449,6 +450,17 @@ void WholeProjectRefactorer::ApplyRefactoringForGroupVariablesContainer(
project, eventsVariableInstructionTypeSwitcher);
}
void WholeProjectRefactorer::UpdateExtensionNameInExtension(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &sourceExtensionName) {
const EventsFunctionsExtensionBrowser eventsFunctionsExtensionBrowser(
eventsFunctionsExtension);
WholeProjectRefactorer::RenameEventsFunctionsExtension(
project, eventsFunctionsExtension, sourceExtensionName,
eventsFunctionsExtension.GetName(), eventsFunctionsExtensionBrowser);
}
void WholeProjectRefactorer::UpdateExtensionNameInEventsBasedBehavior(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
@@ -497,145 +509,6 @@ void WholeProjectRefactorer::RenameEventsFunctionsExtension(
projectBrowser);
};
auto renameBehaviorEventsFunction =
[&project, &oldName, &newName,
&projectBrowser](const gd::EventsBasedBehavior &eventsBasedBehavior,
const gd::EventsFunction &eventsFunction) {
if (eventsFunction.IsExpression()) {
// Nothing to do, expressions are not including the extension name
}
if (eventsFunction.IsAction() || eventsFunction.IsCondition()) {
gd::InstructionsTypeRenamer renamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
oldName, eventsBasedBehavior.GetName(),
eventsFunction.GetName()),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
newName, eventsBasedBehavior.GetName(),
eventsFunction.GetName()));
projectBrowser.ExposeEvents(project, renamer);
}
};
auto renameBehaviorPropertyFunctions =
[&project, &oldName, &newName,
&projectBrowser](const gd::EventsBasedBehavior &eventsBasedBehavior,
const gd::NamedPropertyDescriptor &property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
oldName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetPropertyActionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
newName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetPropertyActionName(
property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
gd::InstructionsTypeRenamer conditionRenamer =
gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
oldName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetPropertyConditionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
newName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetPropertyConditionName(
property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
// Nothing to do for expressions, expressions are not including the
// extension name
};
auto renameBehaviorSharedPropertyFunctions =
[&project, &oldName, &newName,
&projectBrowser](const gd::EventsBasedBehavior &eventsBasedBehavior,
const gd::NamedPropertyDescriptor &property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
oldName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetSharedPropertyActionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
newName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetSharedPropertyActionName(
property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
gd::InstructionsTypeRenamer conditionRenamer =
gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
oldName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetSharedPropertyConditionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
newName, eventsBasedBehavior.GetName(),
gd::EventsBasedBehavior::GetSharedPropertyConditionName(
property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
// Nothing to do for expressions, expressions are not including the
// extension name
};
auto renameObjectEventsFunction =
[&project, &oldName, &newName,
&projectBrowser](const gd::EventsBasedObject &eventsBasedObject,
const gd::EventsFunction &eventsFunction) {
if (eventsFunction.IsExpression()) {
// Nothing to do, expressions are not including the extension name
}
if (eventsFunction.IsAction() || eventsFunction.IsCondition()) {
gd::InstructionsTypeRenamer renamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
oldName, eventsBasedObject.GetName(),
eventsFunction.GetName()),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
newName, eventsBasedObject.GetName(),
eventsFunction.GetName()));
projectBrowser.ExposeEvents(project, renamer);
}
};
auto renameObjectPropertyFunctions =
[&project, &oldName, &newName,
&projectBrowser](const gd::EventsBasedObject &eventsBasedObject,
const gd::NamedPropertyDescriptor &property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
oldName, eventsBasedObject.GetName(),
gd::EventsBasedObject::GetPropertyActionName(
property.GetName())),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
newName, eventsBasedObject.GetName(),
gd::EventsBasedObject::GetPropertyActionName(
property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
gd::InstructionsTypeRenamer conditionRenamer =
gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
oldName, eventsBasedObject.GetName(),
gd::EventsBasedObject::GetPropertyConditionName(
property.GetName())),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
newName, eventsBasedObject.GetName(),
gd::EventsBasedObject::GetPropertyConditionName(
property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
// Nothing to do for expressions, expressions are not including the
// extension name
};
// Order is important: we first rename the expressions then the instructions,
// to avoid being unable to fetch the metadata (the types of parameters) of
// instructions after they are renamed.
@@ -647,16 +520,6 @@ void WholeProjectRefactorer::RenameEventsFunctionsExtension(
renameEventsFunction(*eventsFunction);
}
}
// Behavior expressions
for (auto &&eventsBasedBehavior :
eventsFunctionsExtension.GetEventsBasedBehaviors().GetInternalVector()) {
auto &behaviorEventsFunctions = eventsBasedBehavior->GetEventsFunctions();
for (auto &&eventsFunction : behaviorEventsFunctions.GetInternalVector()) {
if (eventsFunction->IsExpression()) {
renameBehaviorEventsFunction(*eventsBasedBehavior, *eventsFunction);
}
}
}
// Free instructions
for (auto &&eventsFunction :
@@ -666,73 +529,20 @@ void WholeProjectRefactorer::RenameEventsFunctionsExtension(
}
}
// Behavior instructions
for (auto &&eventsBasedBehavior :
eventsFunctionsExtension.GetEventsBasedBehaviors().GetInternalVector()) {
auto &behaviorEventsFunctions = eventsBasedBehavior->GetEventsFunctions();
for (auto &&eventsFunction : behaviorEventsFunctions.GetInternalVector()) {
if (eventsFunction->IsAction() || eventsFunction->IsCondition()) {
renameBehaviorEventsFunction(*eventsBasedBehavior, *eventsFunction);
}
}
WholeProjectRefactorer::MoveEventsBasedBehavior(
project, oldName, newName, *eventsBasedBehavior,
eventsBasedBehavior->GetName(), eventsBasedBehavior->GetName(),
projectBrowser);
}
// Behavior properties
for (auto &&eventsBasedBehavior :
eventsFunctionsExtension.GetEventsBasedBehaviors().GetInternalVector()) {
for (auto &&propertyDescriptor :
eventsBasedBehavior->GetPropertyDescriptors().GetInternalVector()) {
renameBehaviorPropertyFunctions(*eventsBasedBehavior,
*propertyDescriptor);
}
for (auto &&propertyDescriptor :
eventsBasedBehavior->GetSharedPropertyDescriptors()
.GetInternalVector()) {
renameBehaviorSharedPropertyFunctions(*eventsBasedBehavior,
*propertyDescriptor);
}
}
// Object instructions
for (auto &&eventsBasedObject :
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
auto &objectEventsFunctions = eventsBasedObject->GetEventsFunctions();
for (auto &&eventsFunction : objectEventsFunctions.GetInternalVector()) {
if (eventsFunction->IsAction() || eventsFunction->IsCondition()) {
renameObjectEventsFunction(*eventsBasedObject, *eventsFunction);
}
}
}
// Object properties
for (auto &&eventsBasedObject :
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
auto &objectProperties = eventsBasedObject->GetPropertyDescriptors();
for (auto &&propertyDescriptor : objectProperties.GetInternalVector()) {
renameObjectPropertyFunctions(*eventsBasedObject, *propertyDescriptor);
}
}
// Finally, rename behaviors used in objects
for (auto &&eventsBasedBehavior :
eventsFunctionsExtension.GetEventsBasedBehaviors().GetInternalVector()) {
DoRenameBehavior(project,
gd::PlatformExtension::GetBehaviorFullType(
oldName, eventsBasedBehavior->GetName()),
gd::PlatformExtension::GetBehaviorFullType(
newName, eventsBasedBehavior->GetName()),
projectBrowser);
}
// Finally, rename custom objects type
for (auto &&eventsBasedObject :
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
DoRenameObject(project,
gd::PlatformExtension::GetObjectFullType(
oldName, eventsBasedObject->GetName()),
gd::PlatformExtension::GetObjectFullType(
newName, eventsBasedObject->GetName()),
projectBrowser);
WholeProjectRefactorer::MoveEventsBasedObject(
project, oldName, newName, *eventsBasedObject,
eventsBasedObject->GetName(), eventsBasedObject->GetName(),
projectBrowser);
}
}
@@ -740,6 +550,19 @@ void WholeProjectRefactorer::RenameEventsFunction(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldFunctionName, const gd::String &newFunctionName) {
WholeProjectRefactorer::MoveEventsFunction(
project,
eventsFunctionsExtension,
eventsFunctionsExtension.GetName(),
eventsFunctionsExtension.GetName(),
oldFunctionName, newFunctionName);
}
void WholeProjectRefactorer::MoveEventsFunction(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldFunctionName, const gd::String &newFunctionName) {
const auto &eventsFunctions = eventsFunctionsExtension.GetEventsFunctions();
if (!eventsFunctions.HasEventsFunctionNamed(oldFunctionName))
return;
@@ -751,9 +574,9 @@ void WholeProjectRefactorer::RenameEventsFunction(
DoRenameEventsFunction(
project, eventsFunction,
gd::PlatformExtension::GetEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldFunctionName),
oldExtensionName, oldFunctionName),
gd::PlatformExtension::GetEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newFunctionName),
newExtensionName, newFunctionName),
wholeProjectExposer);
if (eventsFunction.GetFunctionType() ==
@@ -1500,8 +1323,9 @@ void WholeProjectRefactorer::UpdateBehaviorNameInEventsBasedBehavior(
const gd::String &sourceBehaviorName) {
const EventBasedBehaviorBrowser eventBasedBehaviorExposer(
eventsFunctionsExtension, eventsBasedBehavior);
WholeProjectRefactorer::RenameEventsBasedBehavior(
project, eventsFunctionsExtension, eventsBasedBehavior,
WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsFunctionsExtension.GetName(),
eventsFunctionsExtension.GetName(), eventsBasedBehavior,
sourceBehaviorName, eventsBasedBehavior.GetName(),
eventBasedBehaviorExposer);
}
@@ -1519,21 +1343,44 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
}
auto &eventsBasedBehavior = eventsBasedBehaviors.Get(oldBehaviorName);
const WholeProjectBrowser projectBrowser;
WholeProjectRefactorer::RenameEventsBasedBehavior(
project, eventsFunctionsExtension, eventsBasedBehavior, oldBehaviorName,
WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsFunctionsExtension.GetName(),
eventsFunctionsExtension.GetName(), eventsBasedBehavior, oldBehaviorName,
newBehaviorName, projectBrowser);
}
void WholeProjectRefactorer::RenameEventsBasedBehavior(
void WholeProjectRefactorer::MoveEventsBasedBehavior(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldBehaviorName, const gd::String &newBehaviorName) {
auto &eventsBasedBehaviors =
eventsFunctionsExtension.GetEventsBasedBehaviors();
if (!eventsBasedBehaviors.Has(oldBehaviorName)) {
gd::LogWarning("Warning, " + oldBehaviorName +
" was not found when calling RenameEventsBasedBehavior.");
return;
}
auto &eventsBasedBehavior = eventsBasedBehaviors.Get(oldBehaviorName);
const WholeProjectBrowser projectBrowser;
WholeProjectRefactorer::MoveEventsBasedBehavior(
project, oldExtensionName,
newExtensionName, eventsBasedBehavior, oldBehaviorName,
newBehaviorName, projectBrowser);
}
void WholeProjectRefactorer::MoveEventsBasedBehavior(
gd::Project &project,
const gd::String &oldExtensionName,
const gd::String &newExtensionName,
const gd::EventsBasedBehavior &eventsBasedBehavior,
const gd::String &oldBehaviorName,
const gd::String &newBehaviorName,
const gd::ProjectBrowser &projectBrowser) {
auto renameBehaviorEventsFunction =
[&project, &eventsFunctionsExtension, &oldBehaviorName,
&newBehaviorName, &projectBrowser](const gd::EventsFunction &eventsFunction) {
[&project, &oldExtensionName, &newExtensionName, &oldBehaviorName,
&newBehaviorName,
&projectBrowser](const gd::EventsFunction &eventsFunction) {
if (eventsFunction.IsExpression()) {
// Nothing to do, expressions are not including the name of the
// behavior
@@ -1542,36 +1389,36 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
gd::InstructionsTypeRenamer renamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName,
oldExtensionName, oldBehaviorName,
eventsFunction.GetName()),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newBehaviorName,
newExtensionName, newBehaviorName,
eventsFunction.GetName()));
projectBrowser.ExposeEvents(project, renamer);
}
};
auto renameBehaviorProperty = [&project, &eventsFunctionsExtension,
auto renameBehaviorProperty = [&project, &oldExtensionName, &newExtensionName,
&oldBehaviorName, &newBehaviorName, &projectBrowser](
const gd::NamedPropertyDescriptor
&property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName,
oldExtensionName, oldBehaviorName,
EventsBasedBehavior::GetPropertyActionName(property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newBehaviorName,
newExtensionName, newBehaviorName,
EventsBasedBehavior::GetPropertyActionName(property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
gd::InstructionsTypeRenamer conditionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName,
oldExtensionName, oldBehaviorName,
EventsBasedBehavior::GetPropertyConditionName(property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newBehaviorName,
newExtensionName, newBehaviorName,
EventsBasedBehavior::GetPropertyConditionName(property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
@@ -1580,16 +1427,16 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
};
auto renameBehaviorSharedProperty =
[&project, &eventsFunctionsExtension, &oldBehaviorName,
[&project, &oldExtensionName, &newExtensionName, &oldBehaviorName,
&newBehaviorName, &projectBrowser](const gd::NamedPropertyDescriptor &property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName,
oldExtensionName, oldBehaviorName,
EventsBasedBehavior::GetSharedPropertyActionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newBehaviorName,
newExtensionName, newBehaviorName,
EventsBasedBehavior::GetSharedPropertyActionName(
property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
@@ -1598,11 +1445,11 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName,
oldExtensionName, oldBehaviorName,
EventsBasedBehavior::GetSharedPropertyConditionName(
property.GetName())),
gd::PlatformExtension::GetBehaviorEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newBehaviorName,
newExtensionName, newBehaviorName,
EventsBasedBehavior::GetSharedPropertyConditionName(
property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
@@ -1619,6 +1466,8 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
// Behavior expressions
for (auto &&eventsFunction : behaviorEventsFunctions.GetInternalVector()) {
if (eventsFunction->IsExpression()) {
// This does nothing in practice.
// Expressions are not including the name of the behavior.
renameBehaviorEventsFunction(*eventsFunction);
}
}
@@ -1642,9 +1491,9 @@ void WholeProjectRefactorer::RenameEventsBasedBehavior(
DoRenameBehavior(project,
gd::PlatformExtension::GetBehaviorFullType(
eventsFunctionsExtension.GetName(), oldBehaviorName),
oldExtensionName, oldBehaviorName),
gd::PlatformExtension::GetBehaviorFullType(
eventsFunctionsExtension.GetName(), newBehaviorName),
newExtensionName, newBehaviorName),
projectBrowser);
}
@@ -1655,8 +1504,9 @@ void WholeProjectRefactorer::UpdateObjectNameInEventsBasedObject(
const gd::String &sourceObjectName) {
const EventBasedObjectBrowser eventBasedObjectBrowser(
eventsFunctionsExtension, eventsBasedObject);
WholeProjectRefactorer::RenameEventsBasedObject(
project, eventsFunctionsExtension, eventsBasedObject,
WholeProjectRefactorer::MoveEventsBasedObject(
project, eventsFunctionsExtension.GetName(),
eventsFunctionsExtension.GetName(), eventsBasedObject,
sourceObjectName, eventsBasedObject.GetName(),
eventBasedObjectBrowser);
}
@@ -1665,6 +1515,18 @@ void WholeProjectRefactorer::RenameEventsBasedObject(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldObjectName, const gd::String &newObjectName) {
WholeProjectRefactorer::MoveEventsBasedObject(
project,
eventsFunctionsExtension,
eventsFunctionsExtension.GetName(), eventsFunctionsExtension.GetName(),
oldObjectName, newObjectName);
}
void WholeProjectRefactorer::MoveEventsBasedObject(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldObjectName, const gd::String &newObjectName) {
auto &eventsBasedObjects = eventsFunctionsExtension.GetEventsBasedObjects();
if (!eventsBasedObjects.Has(oldObjectName)) {
gd::LogWarning("Warning, " + oldObjectName +
@@ -1673,19 +1535,20 @@ void WholeProjectRefactorer::RenameEventsBasedObject(
}
auto &eventsBasedObject = eventsBasedObjects.Get(oldObjectName);
const WholeProjectBrowser projectBrowser;
WholeProjectRefactorer::RenameEventsBasedObject(
project, eventsFunctionsExtension, eventsBasedObject, oldObjectName,
WholeProjectRefactorer::MoveEventsBasedObject(
project, oldExtensionName,
newExtensionName, eventsBasedObject, oldObjectName,
newObjectName, projectBrowser);
}
void WholeProjectRefactorer::RenameEventsBasedObject(
void WholeProjectRefactorer::MoveEventsBasedObject(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::EventsBasedObject &eventsBasedObject,
const gd::String &oldObjectName, const gd::String &newObjectName,
const gd::ProjectBrowser &projectBrowser) {
auto renameObjectEventsFunction =
[&project, &eventsFunctionsExtension, &oldObjectName, &newObjectName,
[&project, &oldExtensionName, &newExtensionName, &oldObjectName, &newObjectName,
&projectBrowser](const gd::EventsFunction &eventsFunction) {
if (eventsFunction.IsExpression()) {
// Nothing to do, expressions are not including the name of the
@@ -1695,35 +1558,35 @@ void WholeProjectRefactorer::RenameEventsBasedObject(
gd::InstructionsTypeRenamer renamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldObjectName,
oldExtensionName, oldObjectName,
eventsFunction.GetName()),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newObjectName,
newExtensionName, newObjectName,
eventsFunction.GetName()));
projectBrowser.ExposeEvents(project, renamer);
}
};
auto renameObjectProperty = [&project, &eventsFunctionsExtension,
auto renameObjectProperty = [&project, &oldExtensionName, &newExtensionName,
&oldObjectName, &newObjectName, &projectBrowser](
const gd::NamedPropertyDescriptor &property) {
gd::InstructionsTypeRenamer actionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldObjectName,
oldExtensionName, oldObjectName,
EventsBasedObject::GetPropertyActionName(property.GetName())),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newObjectName,
newExtensionName, newObjectName,
EventsBasedObject::GetPropertyActionName(property.GetName())));
projectBrowser.ExposeEvents(project, actionRenamer);
gd::InstructionsTypeRenamer conditionRenamer = gd::InstructionsTypeRenamer(
project,
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), oldObjectName,
oldExtensionName, oldObjectName,
EventsBasedObject::GetPropertyConditionName(property.GetName())),
gd::PlatformExtension::GetObjectEventsFunctionFullType(
eventsFunctionsExtension.GetName(), newObjectName,
newExtensionName, newObjectName,
EventsBasedObject::GetPropertyConditionName(property.GetName())));
projectBrowser.ExposeEvents(project, conditionRenamer);
@@ -1758,9 +1621,9 @@ void WholeProjectRefactorer::RenameEventsBasedObject(
DoRenameObject(project,
gd::PlatformExtension::GetObjectFullType(
eventsFunctionsExtension.GetName(), oldObjectName),
oldExtensionName, oldObjectName),
gd::PlatformExtension::GetObjectFullType(
eventsFunctionsExtension.GetName(), newObjectName),
newExtensionName, newObjectName),
projectBrowser);
}
@@ -1891,7 +1754,7 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
// Rename object in the current layout
gd::EventsRefactorer::RenameObjectInEvents(
project.GetCurrentPlatform(), projectScopedContainers, layout.GetEvents(),
layout.GetObjects(), oldName, newName);
targetedObjectsContainer, oldName, newName);
// Object groups can't have instances or be in other groups
if (!isObjectGroup) {
@@ -1908,7 +1771,7 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInScene(
auto &externalEvents = project.GetExternalEvents(externalEventsName);
gd::EventsRefactorer::RenameObjectInEvents(
project.GetCurrentPlatform(), projectScopedContainers,
externalEvents.GetEvents(), layout.GetObjects(), oldName, newName);
externalEvents.GetEvents(), targetedObjectsContainer, oldName, newName);
}
// Rename object in external layouts
+56 -8
View File
@@ -125,6 +125,15 @@ class GD_CORE_API WholeProjectRefactorer {
const gd::String& oldName,
const gd::String& newName);
/**
* \brief Refactor extension events after the extension has been pasted or
* duplicated.
*/
static void UpdateExtensionNameInExtension(
gd::Project &project,
gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &sourceExtensionName);
/**
* \brief Refactor behavior events after the behavior has been placed in a new
* extension.
@@ -158,6 +167,19 @@ class GD_CORE_API WholeProjectRefactorer {
const gd::String& oldFunctionName,
const gd::String& newFunctionName);
/**
* \brief Refactor the project **before** an events function is moved.
*
* \warning Do move the specified function after calling this.
* This is because the function is expected to have its old name for the
* refactoring.
*/
static void MoveEventsFunction(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldFunctionName, const gd::String &newFunctionName);
/**
* \brief Refactor the project **before** an events function of a behavior is
* renamed.
@@ -384,6 +406,19 @@ class GD_CORE_API WholeProjectRefactorer {
const gd::String& oldBehaviorName,
const gd::String& newBehaviorName);
/**
* \brief Refactor the project **before** a behavior is moved.
*
* \warning Move the specified behavior after calling this.
* This is because the behavior is expected to have its old name for the
* refactoring.
*/
static void MoveEventsBasedBehavior(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldBehaviorName, const gd::String &newBehaviorName);
/**
* \brief Refactor events-based behavior events after the events-based
* behavior has been duplicated.
@@ -402,10 +437,22 @@ class GD_CORE_API WholeProjectRefactorer {
* refactoring.
*/
static void RenameEventsBasedObject(
gd::Project& project,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::String& oldObjectName,
const gd::String& newObjectName);
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldObjectName, const gd::String &newObjectName);
/**
* \brief Refactor the project **before** an object is moved.
*
* \warning Do move the specified object after calling this.
* This is because the object is expected to have its old name for the
* refactoring.
*/
static void MoveEventsBasedObject(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::String &oldObjectName, const gd::String &newObjectName);
/**
* \brief Refactor events-based object events after the events-based object
@@ -770,9 +817,10 @@ class GD_CORE_API WholeProjectRefactorer {
* This is because the behavior is expected to have its old name for the
* refactoring.
*/
static void RenameEventsBasedBehavior(
static void MoveEventsBasedBehavior(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName,
const gd::String &newExtensionName,
const gd::EventsBasedBehavior &eventsBasedBehavior,
const gd::String &oldBehaviorName,
const gd::String &newBehaviorName,
@@ -785,9 +833,9 @@ class GD_CORE_API WholeProjectRefactorer {
* This is because the object is expected to have its old name for the
* refactoring.
*/
static void RenameEventsBasedObject(
static void MoveEventsBasedObject(
gd::Project &project,
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::String &oldExtensionName, const gd::String &newExtensionName,
const gd::EventsBasedObject &eventsBasedObject,
const gd::String &oldObjectName, const gd::String &newObjectName,
const gd::ProjectBrowser &projectBrowser);
@@ -88,4 +88,10 @@ void EventsBasedObject::UnserializeDefaultVariantFrom(
defaultVariant.SetName("");
}
bool EventsBasedObject::IsObjectLifecycleEventsFunction(
const gd::String &functionName) {
return functionName == "onCreated" || functionName == "doStepPostEvents" ||
functionName == "onDestroy" || functionName == "onHotReloading";
}
} // namespace gd
+13 -1
View File
@@ -391,7 +391,19 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
void UnserializeDefaultVariantFrom(gd::Project &project,
const SerializerElement &element);
private:
/** \name Lifecycle event functions
*/
///@{
/**
* Check if the name of the function is the name of a lifecycle function (for
* events-based objects), that will be called automatically by the game
* engine.
*/
static bool
IsObjectLifecycleEventsFunction(const gd::String &eventsFunctionName);
///@}
private:
gd::String defaultName;
gd::String assetStoreTag;
bool isRenderedIn3D;
@@ -58,6 +58,7 @@ void EventsFunctionsContainer::AddMissingFunctionsInRootFolder() {
}
}
const gd::String &group = groupSource->GetGroup();
// TODO Handle sub-folders
auto &folder = !group.empty() ? rootFolder->GetOrCreateChildFolder(group)
: *rootFolder;
folder.InsertFunction(&function);
@@ -225,7 +225,7 @@ public:
* Don't forget to update me if members were changed!
*/
void Init(const gd::EventsFunctionsContainer& other) {
// The properties folders are not copied.
// The function folders are not copied.
// It's not an issue because the UI uses the serialization for duplication.
rootFolder = gd::make_unique<gd::FunctionFolderOrFunction>("__ROOT");
SerializableWithNameList<gd::EventsFunction>::Init(other);
+54 -111
View File
@@ -448,58 +448,30 @@ gd::String GD_CORE_API GetTypeOfObject(const gd::ObjectsContainer& project,
// objects. Search "groups is the intersection of its objects" in the
// codebase.
else if (searchInGroups) {
for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) {
if (layout.GetObjectGroups()[i].GetName() == name) {
// A group has the name searched
// Verifying now that all objects have the same type.
vector<gd::String> groupsObjects =
layout.GetObjectGroups()[i].GetAllObjectsNames();
gd::String previousType =
groupsObjects.empty()
? ""
: GetTypeOfObject(project, layout, groupsObjects[0], false);
for (std::size_t j = 0; j < groupsObjects.size(); ++j) {
if (GetTypeOfObject(project, layout, groupsObjects[j], false) !=
previousType)
return ""; // The group has more than one type.
}
if (!type.empty() && previousType != type)
return ""; // The group has objects of different type, so the group
// has not any type.
type = previousType;
}
// A group of the layout shadows a global group with the same name: only
// the most local group is considered (as done for objects, and by
// gd::ObjectsContainersList::ExpandObjectName when generating code).
const gd::ObjectsContainer* container = nullptr;
if (layout.GetObjectGroups().Has(name)) {
container = &layout;
} else if (project.GetObjectGroups().Has(name)) {
container = &project;
}
for (std::size_t i = 0; i < project.GetObjectGroups().size(); ++i) {
if (project.GetObjectGroups()[i].GetName() == name) {
// A group has the name searched
// Verifying now that all objects have the same type.
vector<gd::String> groupsObjects =
project.GetObjectGroups()[i].GetAllObjectsNames();
gd::String previousType =
groupsObjects.empty()
? ""
: GetTypeOfObject(project, layout, groupsObjects[0], false);
for (std::size_t j = 0; j < groupsObjects.size(); ++j) {
if (GetTypeOfObject(project, layout, groupsObjects[j], false) !=
previousType)
return ""; // The group has more than one type.
}
if (!type.empty() && previousType != type)
return ""; // The group has objects of different type, so the group
// has not any type.
type = previousType;
if (container) {
// Verifying now that all objects have the same type.
const vector<gd::String>& groupsObjects =
container->GetObjectGroups().Get(name).GetAllObjectsNames();
// Empty groups don't have any type.
if (groupsObjects.empty()) {
return "";
}
type = GetTypeOfObject(project, layout, groupsObjects[0], false);
for (std::size_t j = 1; j < groupsObjects.size(); ++j) {
if (GetTypeOfObject(project, layout, groupsObjects[j], false) != type)
return ""; // The group has more than one type.
}
}
}
return type;
}
@@ -828,70 +800,41 @@ GetBehaviorsOfObject(const gd::ObjectsContainer& project,
// Currently, a group is considered as the "intersection" of all of its
// objects. Search "groups is the intersection of its objects" in the
// codebase.
if (searchInGroups) {
for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) {
if (layout.GetObjectGroups()[i].GetName() == name) {
// A group has the name searched
// Verifying now that all objects have common behaviors.
vector<gd::String> groupsObjects =
layout.GetObjectGroups()[i].GetAllObjectsNames();
for (std::size_t objectIndex = 0; objectIndex < groupsObjects.size();
++objectIndex) {
auto &objectName = groupsObjects[objectIndex];
if (!layout.HasObjectNamed(objectName) &&
!project.HasObjectNamed(objectName)) {
continue;
}
// Get behaviors of the object of the group and delete behavior which
// are not in commons.
vector<gd::String> objectBehaviors =
GetBehaviorsOfObject(project, layout, objectName, false);
if (!behaviorsAlreadyInserted) {
behaviorsAlreadyInserted = true;
behaviors = objectBehaviors;
} else {
for (std::size_t a = 0; a < behaviors.size(); ++a) {
if (find(objectBehaviors.begin(),
objectBehaviors.end(),
behaviors[a]) == objectBehaviors.end()) {
behaviors.erase(behaviors.begin() + a);
--a;
}
}
}
}
}
if (!behaviorsAlreadyInserted && searchInGroups) {
// A group of the layout shadows a global group with the same name: only
// the most local group is considered (as done for objects, and by
// gd::ObjectsContainersList::ExpandObjectName when generating code).
const gd::ObjectsContainer* container = nullptr;
if (layout.GetObjectGroups().Has(name)) {
container = &layout;
} else if (project.GetObjectGroups().Has(name)) {
container = &project;
}
for (std::size_t i = 0; i < project.GetObjectGroups().size(); ++i) {
if (project.GetObjectGroups()[i].GetName() == name) {
// A group has the name searched
// Verifying now that all objects have common behaviors.
vector<gd::String> groupsObjects =
project.GetObjectGroups()[i].GetAllObjectsNames();
for (std::size_t objectIndex = 0; objectIndex < groupsObjects.size();
++objectIndex) {
auto &objectName = groupsObjects[objectIndex];
if (!layout.HasObjectNamed(objectName) &&
!project.HasObjectNamed(objectName)) {
continue;
}
// Get behaviors of the object of the group and delete behavior which
// are not in commons.
vector<gd::String> objectBehaviors =
GetBehaviorsOfObject(project, layout, objectName, false);
if (!behaviorsAlreadyInserted) {
behaviorsAlreadyInserted = true;
behaviors = objectBehaviors;
} else {
for (std::size_t a = 0; a < behaviors.size(); ++a) {
if (find(objectBehaviors.begin(),
objectBehaviors.end(),
behaviors[a]) == objectBehaviors.end()) {
behaviors.erase(behaviors.begin() + a);
--a;
}
if (container) {
// Verifying now that all objects have common behaviors.
const vector<gd::String>& groupsObjects =
container->GetObjectGroups().Get(name).GetAllObjectsNames();
for (std::size_t objectIndex = 0; objectIndex < groupsObjects.size();
++objectIndex) {
auto& objectName = groupsObjects[objectIndex];
if (!layout.HasObjectNamed(objectName) &&
!project.HasObjectNamed(objectName)) {
continue;
}
// Get behaviors of the object of the group and delete behavior which
// are not in commons.
vector<gd::String> objectBehaviors =
GetBehaviorsOfObject(project, layout, objectName, false);
if (!behaviorsAlreadyInserted) {
behaviorsAlreadyInserted = true;
behaviors = objectBehaviors;
} else {
for (std::size_t a = 0; a < behaviors.size(); ++a) {
if (find(objectBehaviors.begin(),
objectBehaviors.end(),
behaviors[a]) == objectBehaviors.end()) {
behaviors.erase(behaviors.begin() + a);
--a;
}
}
}
+4
View File
@@ -454,6 +454,8 @@ std::vector<gd::String> GetHiddenLayers(const Layout& layout);
* \note If a group contains only objects of a same type, then the group has
* this type. Otherwise, it is considered as an object without any specific
* type.
* \note A group of the layout shadows a global group with the same name:
* only the group of the layout is considered.
* \deprecated Use gd::ObjectsContainersList::GetTypeOfObject instead.
*
* @return Type of the object/group.
@@ -517,6 +519,8 @@ gd::String GD_CORE_API GetTypeOfBehavior(const ObjectsContainer& game,
* \brief Get behaviors of an object/group
* \note The behaviors of a group are the behaviors which are found in common
* when looking all the objects of the group.
* \note A group of the layout shadows a global group with the same name:
* only the group of the layout is considered.
*
* @return Vector containing names of behaviors
*/
+4 -2
View File
@@ -20,6 +20,8 @@
namespace gd {
std::size_t Object::nameGeneration = 0;
Object::~Object() {}
Object::Object(const gd::String& name_,
@@ -47,7 +49,7 @@ void Object::Init(const gd::Object& object) {
void Object::CopyWithoutConfiguration(const gd::Object& object) {
persistentUuid = object.persistentUuid;
name = object.name;
SetName(object.name);
assetStoreId = object.assetStoreId;
objectVariables = object.objectVariables;
effectsContainer = object.effectsContainer;
@@ -95,7 +97,7 @@ void Object::UnserializeFrom(gd::Project& project,
SetType(element.GetStringAttribute("type"));
assetStoreId = element.GetStringAttribute("assetStoreId");
name = element.GetStringAttribute("name", name, "nom");
SetName(element.GetStringAttribute("name", name, "nom"));
resourcesPreloading = element.GetStringAttribute("resourcesPreloading", "with-scene");
objectVariables.UnserializeFrom(
+14 -1
View File
@@ -100,7 +100,18 @@ class GD_CORE_API Object {
/** \brief Change the name of the object with the name passed as parameter.
*/
void SetName(const gd::String& name_) { name = name_; };
void SetName(const gd::String& name_) {
name = name_;
++nameGeneration;
};
/**
* \brief Return a counter incremented each time any object is renamed.
*
* Used by gd::ObjectsContainer to know when its index of objects by name
* must be rebuilt, so that renaming an object (from anywhere) is always safe.
*/
static std::size_t GetNameGeneration() { return nameGeneration; }
/** \brief Return the name of the object.
*/
@@ -298,6 +309,8 @@ class GD_CORE_API Object {
/** When set to `"manually"`, its resources are not preloaded with the scene. */
gd::String resourcesPreloading = "with-scene";
static std::size_t nameGeneration; ///< See GetNameGeneration.
/**
* Initialize object using another object. Used by copy-ctor and assign-op.
* Don't forget to update me if members were changed!
+30 -15
View File
@@ -37,6 +37,7 @@ ObjectsContainer& ObjectsContainer::operator=(const ObjectsContainer& other) {
void ObjectsContainer::Init(const gd::ObjectsContainer& other) {
sourceType = other.sourceType;
initialObjects = gd::Clone(other.initialObjects);
InvalidateObjectsByName();
objectGroups = other.objectGroups;
// The objects folders are not copied.
// It's not an issue because the UI uses the serialization for duplication.
@@ -84,28 +85,34 @@ void ObjectsContainer::UnserializeObjectsFrom(
std::cout << "WARNING: Unknown object type \"" << type << "\""
<< std::endl;
}
InvalidateObjectsByName();
}
gd::Object* ObjectsContainer::FindObject(const gd::String& name) const {
const std::size_t nameGeneration = gd::Object::GetNameGeneration();
if (!objectsByNameUpToDate || objectsByNameGeneration != nameGeneration) {
objectsByName.clear();
for (const auto& object : initialObjects) {
// If several objects have the same name, the first one wins (as it
// would with a linear search).
objectsByName.emplace(object->GetName(), object.get());
}
objectsByNameGeneration = nameGeneration;
objectsByNameUpToDate = true;
}
auto it = objectsByName.find(name);
return it == objectsByName.end() ? nullptr : it->second;
}
bool ObjectsContainer::HasObjectNamed(const gd::String& name) const {
return (find_if(initialObjects.begin(),
initialObjects.end(),
[&](const std::unique_ptr<gd::Object>& object) {
return object->GetName() == name;
}) != initialObjects.end());
return FindObject(name) != nullptr;
}
gd::Object& ObjectsContainer::GetObject(const gd::String& name) {
return *(*find_if(initialObjects.begin(),
initialObjects.end(),
[&](const std::unique_ptr<gd::Object>& object) {
return object->GetName() == name;
}));
return *FindObject(name);
}
const gd::Object& ObjectsContainer::GetObject(const gd::String& name) const {
return *(*find_if(initialObjects.begin(),
initialObjects.end(),
[&](const std::unique_ptr<gd::Object>& object) {
return object->GetName() == name;
}));
return *FindObject(name);
}
gd::Object& ObjectsContainer::GetObject(std::size_t index) {
return *initialObjects[index];
@@ -130,6 +137,7 @@ gd::Object& ObjectsContainer::InsertNewObject(const gd::Project& project,
position < initialObjects.size() ? initialObjects.begin() + position
: initialObjects.end(),
project.CreateObject(objectType, name))));
InvalidateObjectsByName();
rootFolder->InsertObject(&newlyCreatedObject);
@@ -144,6 +152,7 @@ gd::Object& ObjectsContainer::InsertNewObjectInFolder(
std::size_t position) {
gd::Object& newlyCreatedObject = *(*(initialObjects.insert(
initialObjects.end(), project.CreateObject(objectType, name))));
InvalidateObjectsByName();
objectFolderOrObject.InsertObject(&newlyCreatedObject, position);
@@ -156,6 +165,7 @@ gd::Object& ObjectsContainer::InsertObject(const gd::Object& object,
position < initialObjects.size() ? initialObjects.begin() + position
: initialObjects.end(),
std::unique_ptr<gd::Object>(object.Clone()))));
InvalidateObjectsByName();
return newlyCreatedObject;
}
@@ -167,6 +177,7 @@ void ObjectsContainer::MoveObject(std::size_t oldIndex, std::size_t newIndex) {
std::unique_ptr<gd::Object> object = std::move(initialObjects[oldIndex]);
initialObjects.erase(initialObjects.begin() + oldIndex);
initialObjects.insert(initialObjects.begin() + newIndex, std::move(object));
InvalidateObjectsByName();
}
void ObjectsContainer::RemoveObject(const gd::String& name) {
@@ -181,11 +192,13 @@ void ObjectsContainer::RemoveObject(const gd::String& name) {
rootFolder->RemoveRecursivelyObjectNamed(name);
initialObjects.erase(objectIt);
InvalidateObjectsByName();
}
void ObjectsContainer::Clear() {
rootFolder->Clear();
initialObjects.clear();
InvalidateObjectsByName();
}
void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder(
@@ -205,8 +218,10 @@ void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder(
std::unique_ptr<gd::Object> object = std::move(*objectIt);
initialObjects.erase(objectIt);
InvalidateObjectsByName();
newContainer.initialObjects.push_back(std::move(object));
newContainer.InvalidateObjectsByName();
objectFolderOrObject.GetParent().MoveObjectFolderOrObjectToAnotherFolder(
objectFolderOrObject, newParentFolder, newPosition);
+24 -8
View File
@@ -8,6 +8,7 @@
#include <memory>
#include <vector>
#include <set>
#include <unordered_map>
#include "GDCore/Project/MemoryTrackedRegistry.h"
#include "GDCore/String.h"
#include "GDCore/Project/ObjectGroupsContainer.h"
@@ -173,14 +174,10 @@ class GD_CORE_API ObjectsContainer {
void Clear();
/**
* Provide a raw access to the vector containing the objects
*/
std::vector<std::unique_ptr<gd::Object> >& GetObjects() {
return initialObjects;
}
/**
* Provide a raw access to the vector containing the objects
* Provide a read-only access to the vector containing the objects.
*
* \note Objects must only be added, removed or moved using the methods of
* this class, so that the index of objects by name is kept up-to-date.
*/
const std::vector<std::unique_ptr<gd::Object> >& GetObjects() const {
return initialObjects;
@@ -255,6 +252,25 @@ class GD_CORE_API ObjectsContainer {
std::unique_ptr<gd::ObjectFolderOrObject> rootFolder;
gd::MemoryTracked _memoryTracked{this, "ObjectsContainer"};
/**
* Objects indexed by name, for fast lookups. Lazily (re)built by
* `FindObject` when marked as outdated (after any change to `initialObjects`)
* or when any object was renamed (see gd::Object::GetNameGeneration).
*/
mutable std::unordered_map<gd::String, gd::Object*> objectsByName;
mutable std::size_t objectsByNameGeneration = 0;
mutable bool objectsByNameUpToDate = false;
/**
* Return the object called \a name, or nullptr if not found.
*/
gd::Object* FindObject(const gd::String& name) const;
/**
* Must be called after any change to `initialObjects`.
*/
void InvalidateObjectsByName() { objectsByNameUpToDate = false; }
/**
* Initialize from another variables container, copying elements. Used by
* copy-ctor and assign-op. Don't forget to update me if members were changed!
+1 -1
View File
@@ -231,7 +231,7 @@ const gd::EventsBasedBehavior& Project::GetEventsBasedBehavior(
}
std::shared_ptr<gd::BaseEvent> Project::CreateEvent(
const gd::String& type, const gd::String& platformName) {
const gd::String& type, const gd::String& platformName) const {
for (std::size_t i = 0; i < platforms.size(); ++i) {
if (!platformName.empty() && platforms[i]->GetName() != platformName)
continue;
+1 -1
View File
@@ -569,7 +569,7 @@ class GD_CORE_API Project {
* first platform supporting the object is used.
*/
std::shared_ptr<gd::BaseEvent> CreateEvent(
const gd::String& type, const gd::String& platformName = "");
const gd::String& type, const gd::String& platformName = "") const;
///@}
/** \name Layouts management
+1 -1
View File
@@ -18,7 +18,7 @@ PropertiesContainer::PropertiesContainer(const PropertiesContainer &other)
: properties(other.properties), owner(other.owner) {
// The properties folders are not copied.
// It's not an issue because the UI uses the serialization for duplication.
rootFolder = gd::make_unique<gd::PropertyFolderOrProperty>("__ROOT");
rootFolder = gd::make_unique<gd::PropertyFolderOrProperty>("__ROOT");;
}
PropertiesContainer &
+5 -3
View File
@@ -324,12 +324,10 @@ void Variable::UnserializeFrom(const SerializerElement& element) {
} else if (type == Type::Boolean) {
SetBool(element.GetBoolAttribute("value", false, "Value"));
}
} else {
} else if (type == Type::Structure || type == Type::Array) {
const SerializerElement& childrenElement =
element.GetChild("children", 0, "Children");
childrenElement.ConsiderAsArrayOf("variable", "Variable");
if (childrenElement.GetChildrenCount() == 0) return;
for (int i = 0; i < childrenElement.GetChildrenCount(); ++i) {
const SerializerElement& childElement = childrenElement.GetChild(i);
if (type == Type::Structure) {
@@ -340,6 +338,10 @@ void Variable::UnserializeFrom(const SerializerElement& element) {
PushNew().UnserializeFrom(childElement);
}
}
// The editor-only "mixed values" marker is restored last, and whatever the
// type is: a variable marked as having mixed values has no children to read
// (they are cleared by MarkAsMixedValues), and a variable with entirely
// mixed types ("mixed" type) has no value and no children either.
if (element.GetBoolAttribute("hasMixedValues", false)) {
MarkAsMixedValues();
}
@@ -0,0 +1,527 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering common features of GDevelop Core.
*/
#include "GDCore/IDE/ExtensionEditor/ChildObjectForwardFunctionGenerator.h"
#include "DummyPlatform.h"
#include "GDCore/Events/Builtin/CommentEvent.h"
#include "GDCore/Events/Builtin/StandardEvent.h"
#include "GDCore/Extensions/Metadata/ValueTypeMetadata.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/IDE/WholeProjectRefactorer.h"
#include "GDCore/Project/EventsBasedBehavior.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Project.h"
#include "GDCore/String.h"
#include "catch.hpp"
TEST_CASE("ChildObjectForwardFunctionGenerator", "[common]") {
SECTION("Can generate a function to forward an action") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyFunction", 0);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Action);
childFunction.SetFullName("My function");
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyStringParameter");
parameter.SetType("string");
}
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyNumberParameter");
parameter.SetType("number");
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MyFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MyFunction");
REQUIRE(parentFunction.GetFullName() == "My function");
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 3);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetParameters().GetParameter(1).GetType() ==
"string");
REQUIRE(parentFunction.GetParameters().GetParameter(2).GetType() ==
"number");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 0);
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MyFunction");
REQUIRE(action.GetParametersCount() == 3);
REQUIRE(action.GetParameter(0).GetPlainString() == "MyChildObject");
REQUIRE(action.GetParameter(1).GetPlainString() == "MyStringParameter");
REQUIRE(action.GetParameter(2).GetPlainString() == "MyNumberParameter");
}
SECTION("Can generate a function to forward a condition") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyFunction", 0);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Condition);
childFunction.SetFullName("My function");
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyStringParameter");
parameter.SetType("string");
}
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyNumberParameter");
parameter.SetType("number");
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MyFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MyFunction");
REQUIRE(parentFunction.GetFullName() == "My function");
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 3);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetParameters().GetParameter(1).GetType() ==
"string");
REQUIRE(parentFunction.GetParameters().GetParameter(2).GetType() ==
"number");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 1);
auto &condition = event.GetInstructionList("conditions")->Get(0);
REQUIRE(condition.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MyFunction");
REQUIRE(condition.GetParametersCount() == 3);
REQUIRE(condition.GetParameter(0).GetPlainString() == "MyChildObject");
REQUIRE(condition.GetParameter(1).GetPlainString() == "MyStringParameter");
REQUIRE(condition.GetParameter(2).GetPlainString() == "MyNumberParameter");
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() == "SetReturnBoolean");
REQUIRE(action.GetParametersCount() == 1);
REQUIRE(action.GetParameter(0).GetPlainString() == "True");
}
SECTION("Can generate a function to forward an expression") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyFunction", 0);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Expression);
childFunction.SetFullName("My function");
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyStringParameter");
parameter.SetType("string");
}
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyNumberParameter");
parameter.SetType("number");
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MyFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MyFunction");
REQUIRE(parentFunction.GetFullName() == "My function");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::Expression);
REQUIRE(parentFunction.GetExpressionType().IsNumber());
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 3);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetParameters().GetParameter(1).GetType() ==
"string");
REQUIRE(parentFunction.GetParameters().GetParameter(2).GetType() ==
"number");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 0);
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() == "SetReturnNumber");
REQUIRE(action.GetParametersCount() == 1);
REQUIRE(action.GetParameter(0).GetPlainString() ==
"MyChildObject.MyFunction(MyStringParameter, MyNumberParameter)");
}
SECTION("Can generate a function to forward a getter and setter") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
{
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyGetterFunction", 0);
childFunction.SetFunctionType(
gd::EventsFunction::FunctionType::ExpressionAndCondition);
childFunction.GetExpressionType().SetName("number");
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
}
{
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MySetterFunction", 1);
childFunction.SetFunctionType(
gd::EventsFunction::FunctionType::ActionWithOperator);
childFunction.SetGetterName("MyGetterFunction");
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
{
REQUIRE(
parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MyGetterFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MyGetterFunction");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::ExpressionAndCondition);
REQUIRE(parentFunction.GetExpressionType().IsNumber());
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 1);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 0);
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() == "SetReturnNumber");
REQUIRE(action.GetParametersCount() == 1);
REQUIRE(action.GetParameter(0).GetPlainString() ==
"MyChildObject.MyGetterFunction()");
}
{
REQUIRE(
parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MySetterFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MySetterFunction");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::ActionWithOperator);
REQUIRE(parentFunction.GetGetterName() == "MyGetterFunction");
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 1);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 0);
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MySetterFunction");
REQUIRE(action.GetParametersCount() == 3);
REQUIRE(action.GetParameter(0).GetPlainString() == "MyChildObject");
REQUIRE(action.GetParameter(1).GetPlainString() == "=");
REQUIRE(action.GetParameter(2).GetPlainString() == "Value");
}
}
SECTION("Can generate a function to forward a boolean setter") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MySetterFunction", 1);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Action);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
{
auto &parameter = childFunction.GetParameters().AddNewParameter("Value");
parameter.SetType("yesorno");
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MySetterFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MySetterFunction");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::Action);
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 2);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetParameters().GetParameter(1).GetType() ==
"yesorno");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 2);
{
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 1);
auto &condition = event.GetInstructionList("conditions")->Get(0);
REQUIRE(condition.GetType() == "BooleanVariable");
REQUIRE(condition.GetParametersCount() == 3);
REQUIRE(condition.GetParameter(0).GetPlainString() == "Value");
REQUIRE(condition.GetParameter(1).GetPlainString() == "False");
REQUIRE(condition.GetParameter(2).GetPlainString() == "");
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MySetterFunction");
REQUIRE(action.GetParametersCount() == 2);
REQUIRE(action.GetParameter(0).GetPlainString() == "MyChildObject");
REQUIRE(action.GetParameter(1).GetPlainString() == "no");
}
{
auto &event = parentFunction.GetEvents().GetEvent(1);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 1);
auto &condition = event.GetInstructionList("conditions")->Get(0);
REQUIRE(condition.GetType() == "BooleanVariable");
REQUIRE(condition.GetParametersCount() == 3);
REQUIRE(condition.GetParameter(0).GetPlainString() == "Value");
REQUIRE(condition.GetParameter(1).GetPlainString() == "True");
REQUIRE(condition.GetParameter(2).GetPlainString() == "");
REQUIRE(event.GetInstructionList("actions")->GetCount() == 1);
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MySetterFunction");
REQUIRE(action.GetParametersCount() == 2);
REQUIRE(action.GetParameter(0).GetPlainString() == "MyChildObject");
REQUIRE(action.GetParameter(1).GetPlainString() == "yes");
}
}
SECTION("Can generate a todo comment for function with boolean parameters") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MySetterFunction", 1);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Action);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
{
auto &parameter = childFunction.GetParameters().AddNewParameter("Value");
parameter.SetType("yesorno");
}
{
auto &parameter =
childFunction.GetParameters().AddNewParameter("MyNumberParameter");
parameter.SetType("number");
}
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MySetterFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MySetterFunction");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::Action);
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 3);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetParameters().GetParameter(1).GetType() ==
"yesorno");
REQUIRE(parentFunction.GetParameters().GetParameter(2).GetType() ==
"number");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
{
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetType() == "BuiltinCommonInstructions::Comment");
auto &comment = dynamic_cast<gd::CommentEvent &>(event);
REQUIRE(comment.GetComment() ==
"TODO: Please implement this function manually.");
}
}
SECTION("Can generate a function to forward an asynchronous action") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
auto &extension =
project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
auto &parentEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyParentEventsBasedObject", 0);
auto &childEventsBasedObject = extension.GetEventsBasedObjects().InsertNew(
"MyChildEventsBasedObject", 0);
parentEventsBasedObject.GetObjects().InsertNewObject(
project, "MyEventsExtension::MyChildEventsBasedObject", "MyChildObject",
0);
auto &childFunction =
childEventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
"MyFunction", 0);
childFunction.SetFunctionType(gd::EventsFunction::FunctionType::Action);
childFunction.SetAsync(true);
gd::WholeProjectRefactorer::EnsureObjectEventsFunctionsProperParameters(
extension, childEventsBasedObject);
gd::ChildObjectForwardFunctionGenerator::
GenerateChildObjectForwardFunctions(
project, extension, parentEventsBasedObject, "MyChildObject");
REQUIRE(parentEventsBasedObject.GetEventsFunctions().HasEventsFunctionNamed(
"MyFunction"));
auto &parentFunction =
parentEventsBasedObject.GetEventsFunctions().GetEventsFunction(
"MyFunction");
REQUIRE(parentFunction.GetFunctionType() ==
gd::EventsFunction::FunctionType::Action);
REQUIRE(parentFunction.IsAsync());
REQUIRE(parentFunction.GetParameters().GetParametersCount() == 1);
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetType() ==
"object");
REQUIRE(parentFunction.GetParameters().GetParameter(0).GetExtraInfo() ==
"MyEventsExtension::MyParentEventsBasedObject");
REQUIRE(parentFunction.GetEvents().GetEventsCount() == 1);
auto &event = parentFunction.GetEvents().GetEvent(0);
REQUIRE(event.GetInstructionList("conditions")->GetCount() == 0);
REQUIRE(event.GetInstructionList("actions")->GetCount() == 2);
{
auto &action = event.GetInstructionList("actions")->Get(0);
REQUIRE(action.GetType() ==
"MyEventsExtension::MyChildEventsBasedObject::MyFunction");
REQUIRE(action.GetParametersCount() == 1);
REQUIRE(action.GetParameter(0).GetPlainString() == "MyChildObject");
}
{
auto &action = event.GetInstructionList("actions")->Get(1);
REQUIRE(action.GetType() == "BuiltinAsync::ResolveAsyncEventsFunction");
REQUIRE(action.GetParametersCount() == 0);
}
}
}
+12 -1
View File
@@ -5,13 +5,13 @@
*/
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Behavior.h"
#include "GDCore/Project/ObjectConfiguration.h"
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Tools/Localization.h"
#include "GDCore/Events/Builtin/CommentEvent.h"
#include "GDCore/Events/Builtin/StandardEvent.h"
#include "GDCore/Events/Builtin/ForEachChildVariableEvent.h"
#include "GDCore/Events/Builtin/RepeatEvent.h"
@@ -176,6 +176,7 @@ void SetupProjectWithDummyPlatform(gd::Project& project,
commonInstructionsExtension->AddEvent("Else", "Else event", "", "", "", std::make_shared<gd::ElseEvent>());
commonInstructionsExtension->AddEvent("ForEachChildVariable", "For each child variable event", "", "", "", std::make_shared<gd::ForEachChildVariableEvent>());
commonInstructionsExtension->AddEvent("Repeat", "Repeat event", "", "", "", std::make_shared<gd::RepeatEvent>());
commonInstructionsExtension->AddEvent("Comment", "Comment event", "", "", "", std::make_shared<gd::CommentEvent>());
std::shared_ptr<gd::PlatformExtension> baseObjectExtension =
std::shared_ptr<gd::PlatformExtension>(new gd::PlatformExtension);
@@ -527,6 +528,16 @@ void SetupProjectWithDummyPlatform(gd::Project& project,
.AddParameter("string", "")
.AddParameter("expression", "", "", true)
.SetFunctionName("getNumberWith3Params");
extension
->AddStrExpression(
"GetVariableString",
"Get string of object variable",
"",
"",
"")
.AddParameter("object", _("Object parameter"))
.AddParameter("objectvar", _("Variable for object"))
.SetFunctionName("getObjectVariableString");
extension
->AddStrExpression(
"GetStringWith2ObjectParamAnd2ObjectVarParam",
+351 -63
View File
@@ -77,10 +77,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
layout1.GetObjects().InsertNewObject(project, "", "MyObject", 0);
myObject.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
myObject.GetVariables().InsertNew("MyObjectVariable");
myObject.GetVariables()
.InsertNew("MyObjectStructureVariable")
.GetChild("MyChildStructure")
.GetChild("MyChild");
myObject.GetVariables().InsertNew("MyObjectStructureVariable").GetChild("MyChild");
myObject.GetVariables().Get("MyObjectStructureVariable").GetChild("MyChildStructure").GetChild("MyChild");
myObject.GetVariables().Get("MyObjectStructureVariable").GetChild("MyChildStructure").GetChild("MyChildStructure2").GetChild("MyChild");
auto &myGroup =
layout1.GetObjects().GetObjectGroups().InsertNew("MyGroup");
@@ -2099,7 +2098,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
SECTION("in object variable parameter") {
auto node =
parser.ParseExpression("MySceneStructureVariable.MyChild");
parser.ParseExpression("MyObjectStructureVariable.MyChild");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "MyObject");
@@ -2238,7 +2237,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Invalid scene variables (2 levels, child does not exist)") {
@@ -2248,7 +2249,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "variable");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Invalid scene variables (3 levels, child does not exist)") {
@@ -2259,7 +2262,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "variable");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Undeclared legacy scene variables (2 levels, child does not exist)") {
@@ -2283,17 +2288,47 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
SECTION("Undeclared legacy object variables (2 levels, child does not exist)") {
auto node = parser.ParseExpression(
"MyObjectStructureVariable.MyNonExistingChild");
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Undeclared legacy object variables (2 levels, child does not exist) in expression") {
auto node = parser.ParseExpression(
"MyExtension::GetVariableString(MyObject, MyObjectStructureVariable.MyNonExistingChild)");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"string");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Empty group with legacy object variables in expression") {
auto node = parser.ParseExpression(
"MyExtension::GetVariableString(EmptyGroup, MyObjectVariable)");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"string");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Empty group with legacy object variables (2 levels, child does not exist) in expression") {
auto node = parser.ParseExpression(
"MyExtension::GetVariableString(EmptyGroup, MyObjectStructureVariable.MyNonExistingChild)");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"string");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Undeclared legacy object variables (3 levels, child does not exist)") {
auto node = parser.ParseExpression(
"MyObjectStructureVariable.MyNonExistingChild.MyNonExistingChild");
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
@@ -2308,7 +2343,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"objectvar", "MyObject");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Undeclared object variables (3 levels, child does not exist)") {
@@ -2319,7 +2356,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"objectvar", "MyObject");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Undeclared object variables in expressions (2 levels, child does not exist)") {
@@ -2330,7 +2369,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"number|string");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Undeclared object variables in expressions (3 levels, child does not exist)") {
@@ -2342,7 +2383,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"number|string");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Invalid scene variables (2 levels, variable and child do not exist)") {
@@ -2362,9 +2405,8 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"You must enter a number or a text, wrapped inside double quotes (example: \"Hello world\"), or a variable name.");
}
SECTION("Invalid scene variables type in expression (1 level)") {
auto node =
parser.ParseExpression("MySceneStructureVariable");
SECTION("Invalid scene variables structure in expression (1 level)") {
auto node = parser.ParseExpression("MySceneStructureVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
@@ -2374,48 +2416,122 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Invalid scene variables type in expression (2 levels)") {
SECTION("Valid scene variable structure in variable parameter (1 level)") {
auto node = parser.ParseExpression("MySceneStructureVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Invalid scene variables structure in expression (2 levels)") {
auto node =
parser.ParseExpression("MySceneStructureVariable.MyChildStructure");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireNoError(validator);
// TODO Add a fatal error
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Invalid scene variables type in expression (3 levels)") {
SECTION("Valid scene variables structure in variable parameter (2 levels)") {
auto node =
parser.ParseExpression("MySceneStructureVariable.MyChildStructure");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Invalid scene variables structure in expression (3 levels)") {
auto node =
parser.ParseExpression("MySceneStructureVariable.MyChildStructure.MyChildStructure2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireNoError(validator);
// TODO Add a fatal error
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Invalid object variables type in expression (1 level)") {
SECTION("Valid scene variables structure in variable parameter (3 levels)") {
auto node =
parser.ParseExpression("MySceneStructureVariable.MyChildStructure.MyChildStructure2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Invalid object variables structure in expression (1 level)") {
auto node =
parser.ParseExpression("MyObject.MyObjectStructureVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireNoError(validator);
// TODO Add a fatal error
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Invalid object variables type in expression (2 levels)") {
SECTION("Valid object variables structure in variable parameter (1 level)") {
auto node = parser.ParseExpression("MyObjectStructureVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Invalid object variables structure in expression (2 levels)") {
auto node =
parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChildStructure");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Valid object variables structure in variable parameter (2 levels)") {
auto node =
parser.ParseExpression("MyObjectStructureVariable.MyChildStructure");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Invalid object variables structure in expression (3 levels)") {
auto node =
parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChildStructure.MyChildStructure2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You need to specify the name of the child variable to access. For example: `MyVariable.child`.");
}
SECTION("Valid object variables structure in variable parameter (3 levels)") {
auto node =
parser.ParseExpression("MyObjectStructureVariable.MyChildStructure.MyChildStructure2");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar");
node->Visit(validator);
RequireNoError(validator);
// TODO Add a fatal error
}
SECTION("Valid object variables (1 level)") {
@@ -2459,7 +2575,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
SECTION("Valid object variables (2 levels)") {
{
auto node =
parser.ParseExpression("MySpriteObject.MyVariable.MyChild");
parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChild");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
@@ -2467,7 +2583,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
{
auto node =
parser.ParseExpression("MySpriteObject.MyVariable.MyChild + MySpriteObject.MyVariable2");
parser.ParseExpression("MyObject.MyObjectStructureVariable.MyChild + MySpriteObject.MyVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
@@ -2475,7 +2591,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
{
auto node =
parser.ParseExpression("MySpriteObject.MyVariable[\"MyChild\"] + MySpriteObject.MyVariable2");
parser.ParseExpression("MyObject.MyObjectStructureVariable[\"MyChild\"] + MySpriteObject.MyVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
@@ -2499,8 +2615,10 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
// TODO Add a non-fatal error
RequireNoFatalError(validator);
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"No child variable with this name found.");
}
SECTION("Invalid object variables (1 level, non existing object)") {
@@ -2565,17 +2683,50 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
}
}
SECTION("Invalid object variables (empty object group)") {
{
auto node =
parser.ParseExpression("EmptyGroup.MyVariable");
SECTION("Invalid object variables (empty object group, in expression)") {
auto node = parser.ParseExpression("EmptyGroup.MyVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"This group is empty. Add an object to this group first.");
}
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"This group is empty. Add an object to this group first.");
}
SECTION("Invalid object variables with children (empty object group, in expression)") {
auto node =
parser.ParseExpression("EmptyGroup.MyObjectStructureVariable.MyChild");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"number|string");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"This group is empty. Add an object to this group first.");
}
SECTION("Invalid object variables (empty object group)") {
auto node = parser.ParseExpression("MyVariable");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "EmptyGroup");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"This group is empty. Add an object to this group first.");
}
SECTION("Invalid object variables with children (empty object group)") {
auto node =
parser.ParseExpression("MyObjectStructureVariable.MyChild");
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "EmptyGroup");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"This group is empty. Add an object to this group first.");
}
SECTION("Invalid object variables (2 levels, bracket accessor)") {
@@ -3882,7 +4033,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You entered a number, but this type was expected: variable");
"The variable name looks like you're building an expression or a "
"formula. You can only use this for structure or arrays, for "
"example: Score[3].");
}
SECTION("string instead") {
auto node = parser.ParseExpression("\"text\"");
@@ -3892,7 +4045,129 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"You entered a text, but this type was expected: variable");
"The variable name looks like you're building an expression or a "
"formula. You can only use this for structure or arrays, for "
"example: Score[\"Player1\"].");
}
SECTION("Object variable with unary operator") {
auto node = parser.ParseExpression("-MyObjectVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "MyObject");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Object variable with operator") {
auto node = parser.ParseExpression("MyObjectVariable+MyObjectVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "MyObject");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Object children variable with operator") {
auto node = parser.ParseExpression(
"MyObjectStructureVariable.MyChild+MyObjectVariable");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "MyObject");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Object variable with unary operator (legacy and extensions)") {
auto node = parser.ParseExpression("-MyObjectVariable");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Object variable with operator (legacy and extensions)") {
auto node = parser.ParseExpression("MyObjectVariable+MyObjectVariable");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Object children variable with operator (legacy and extensions)") {
auto node = parser.ParseExpression(
"MyObjectStructureVariable.MyChild+MyObjectVariable");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Variable with unary operator") {
auto node = parser.ParseExpression("-MySceneVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Variable with operator") {
auto node = parser.ParseExpression("MySceneVariable+MySceneVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
SECTION("Children variable with operator") {
auto node = parser.ParseExpression(
"MySceneStructureVariable.MyChild+MySceneVariable");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
RequireFatalErrorsCount(validator, 1);
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
"Operators (+, -, /, *) can't be used in variable names. Remove "
"the operator from the variable name.");
}
}
@@ -3929,7 +4204,9 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"objectvar", "MyObject");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"This variable does not exist on this object or group.");
}
SECTION("Undeclared object variable with children") {
@@ -3941,7 +4218,30 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"objectvar", "MyObject");
node->Visit(validator);
RequireNoFatalError(validator);
// TODO Add a non-fatal error
RequireAllErrorsCount(validator, 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"This variable does not exist on this object or group.");
}
SECTION("Undeclared object variable (legacy and extensions)") {
auto node = parser.ParseExpression("MyUndeclaredVariable");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Undeclared object variable with children (legacy and extensions)") {
auto node =
parser.ParseExpression("MyUndeclaredVariable.MyChild.MyChild");
REQUIRE(node != nullptr);
// This is a legacy object variable, we don't pass the object name.
gd::ExpressionValidator validator(platform, projectScopedContainers,
"objectvar", "");
node->Visit(validator);
RequireNoError(validator);
}
SECTION("Undeclared object variable in expression") {
@@ -4324,7 +4624,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
RequireNoError(validator);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
@@ -4335,7 +4634,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
RequireNoError(validator);
}
// A string concatenated with an unknown variable (will have to be casted to a string in code generation)
@@ -4346,8 +4644,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "string");
node->Visit(validator);
RequireNoError(validator);
RequireNoFatalError(validator);
}
}
SECTION("Expression/parent type is 'number'") {
@@ -4367,7 +4664,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
RequireNoError(validator);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
@@ -4378,7 +4674,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
RequireNoError(validator);
}
// A number concatenated with an unknown variable (will have to be casted to a number in code generation)
@@ -4389,8 +4684,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number");
node->Visit(validator);
RequireNoError(validator);
RequireNoFatalError(validator);
}
}
SECTION("Expression/parent type is 'number|string'") {
@@ -4411,7 +4705,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
}
// A string concatenated with a number variable (will have to be casted to a string in code generation)
@@ -4422,7 +4715,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
}
// A string concatenated with an unknown variable (will have to be casted to a string in code generation)
@@ -4433,8 +4725,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
RequireNoFatalError(validator);
}
}
SECTION("Expression/parent inferred type is 'number'") {
@@ -4454,7 +4745,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
}
// A number concatenated with a string variable (will have to be casted to a number in code generation)
@@ -4465,7 +4755,6 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
}
// A number concatenated with an unknown variable (will have to be casted to a number in code generation)
@@ -4476,8 +4765,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
gd::ExpressionValidator validator(platform, projectScopedContainers, "number|string");
node->Visit(validator);
RequireNoError(validator);
RequireNoFatalError(validator);
}
}
}
@@ -8,7 +8,6 @@
#include "DummyPlatform.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "catch.hpp"
@@ -8,7 +8,6 @@
#include "GDCore/Events/Parsers/ExpressionParser2.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/Events/ExpressionValidator.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "catch.hpp"
+325
View File
@@ -0,0 +1,325 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
/**
* @file Tests covering gd::ObjectsContainer, in particular the lookup of
* objects by name, which must stay correct whatever the way objects are
* added, removed, moved or renamed.
*/
#include "GDCore/Project/ObjectsContainer.h"
#include <memory>
#include "DummyPlatform.h"
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectsContainersList.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "catch.hpp"
namespace {
gd::Object &InsertObject(gd::Project &project,
gd::ObjectsContainer &objectsContainer,
const gd::String &name) {
return objectsContainer.InsertNewObject(
project, "MyExtension::Sprite", name,
objectsContainer.GetObjectsCount());
}
} // namespace
TEST_CASE("ObjectsContainer", "[common]") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::ObjectsContainer objectsContainer(gd::ObjectsContainer::Scene);
SECTION("Objects can be found by name once inserted") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
gd::Object &object2 = InsertObject(project, objectsContainer, "Object2");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
REQUIRE(objectsContainer.HasObjectNamed("Object2"));
REQUIRE(!objectsContainer.HasObjectNamed("Object3"));
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
REQUIRE(&objectsContainer.GetObject("Object2") == &object2);
// Insertion after a lookup is also found.
gd::Object &object3 = InsertObject(project, objectsContainer, "Object3");
REQUIRE(objectsContainer.HasObjectNamed("Object3"));
REQUIRE(&objectsContainer.GetObject("Object3") == &object3);
// Lookups on a const container work too.
const gd::ObjectsContainer &constObjectsContainer = objectsContainer;
REQUIRE(constObjectsContainer.HasObjectNamed("Object2"));
REQUIRE(&constObjectsContainer.GetObject("Object2") == &object2);
}
SECTION("Objects renamed directly with gd::Object::SetName are found") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
gd::Object &object2 = InsertObject(project, objectsContainer, "Object2");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
object1.SetName("RenamedObject1");
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(objectsContainer.HasObjectNamed("RenamedObject1"));
REQUIRE(&objectsContainer.GetObject("RenamedObject1") == &object1);
// Swap names between two objects.
object2.SetName("Object1");
object1.SetName("Object2");
REQUIRE(&objectsContainer.GetObject("Object1") == &object2);
REQUIRE(&objectsContainer.GetObject("Object2") == &object1);
REQUIRE(!objectsContainer.HasObjectNamed("RenamedObject1"));
}
SECTION("Objects renamed by unserialization are found") {
gd::Object &object = InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
gd::SerializerElement element;
object.SerializeTo(element);
element.SetAttribute("name", "RenamedObject1");
object.UnserializeFrom(project, element);
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(&objectsContainer.GetObject("RenamedObject1") == &object);
}
SECTION("Objects renamed by copy-assignment are found") {
gd::Object &object = InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
object = *project.CreateObject("MyExtension::Sprite", "RenamedObject1");
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(&objectsContainer.GetObject("RenamedObject1") == &object);
}
SECTION("Removed objects are not found anymore") {
InsertObject(project, objectsContainer, "Object1");
InsertObject(project, objectsContainer, "Object2");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
objectsContainer.RemoveObject("Object1");
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(objectsContainer.HasObjectNamed("Object2"));
// Re-adding an object with the same name gives the new object.
gd::Object &newObject1 = InsertObject(project, objectsContainer, "Object1");
REQUIRE(&objectsContainer.GetObject("Object1") == &newObject1);
objectsContainer.Clear();
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(!objectsContainer.HasObjectNamed("Object2"));
}
SECTION("Objects inserted by copy or in a folder are found") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
gd::Object &object2 = objectsContainer.InsertNewObjectInFolder(
project, "MyExtension::Sprite", "Object2",
objectsContainer.GetRootFolder(), 0);
REQUIRE(&objectsContainer.GetObject("Object2") == &object2);
std::unique_ptr<gd::Object> otherObject =
project.CreateObject("MyExtension::Sprite", "Object3");
gd::Object &object3 = objectsContainer.InsertObject(*otherObject, 0);
REQUIRE(&objectsContainer.GetObject("Object3") == &object3);
REQUIRE(&object3 != otherObject.get());
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
}
SECTION("Moved objects are still found") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
gd::Object &object2 = InsertObject(project, objectsContainer, "Object2");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
objectsContainer.MoveObject(0, 1);
REQUIRE(&objectsContainer.GetObject(0) == &object2);
REQUIRE(&objectsContainer.GetObject(1) == &object1);
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
REQUIRE(&objectsContainer.GetObject("Object2") == &object2);
REQUIRE(objectsContainer.GetObjectPosition("Object1") == 1);
}
SECTION("Objects moved to another container are found in it only") {
gd::Object &object = InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
gd::ObjectsContainer otherObjectsContainer(gd::ObjectsContainer::Scene);
REQUIRE(!otherObjectsContainer.HasObjectNamed("Object1"));
objectsContainer.MoveObjectFolderOrObjectToAnotherContainerInFolder(
objectsContainer.GetRootFolder().GetObjectChild("Object1"),
otherObjectsContainer, otherObjectsContainer.GetRootFolder(), 0);
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(&otherObjectsContainer.GetObject("Object1") == &object);
}
SECTION("Copied containers have their own objects") {
gd::Object &object = InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
gd::ObjectsContainer copiedObjectsContainer(objectsContainer);
REQUIRE(copiedObjectsContainer.HasObjectNamed("Object1"));
REQUIRE(&copiedObjectsContainer.GetObject("Object1") != &object);
gd::ObjectsContainer assignedObjectsContainer(gd::ObjectsContainer::Global);
InsertObject(project, assignedObjectsContainer, "Object2");
REQUIRE(assignedObjectsContainer.HasObjectNamed("Object2"));
assignedObjectsContainer = objectsContainer;
REQUIRE(!assignedObjectsContainer.HasObjectNamed("Object2"));
REQUIRE(assignedObjectsContainer.HasObjectNamed("Object1"));
REQUIRE(&assignedObjectsContainer.GetObject("Object1") != &object);
// Renaming in a copy does not affect the original.
copiedObjectsContainer.GetObject("Object1").SetName("RenamedObject1");
REQUIRE(&objectsContainer.GetObject("Object1") == &object);
REQUIRE(!objectsContainer.HasObjectNamed("RenamedObject1"));
REQUIRE(copiedObjectsContainer.HasObjectNamed("RenamedObject1"));
}
SECTION("Unserialized objects are found") {
InsertObject(project, objectsContainer, "Object1");
REQUIRE(objectsContainer.HasObjectNamed("Object1"));
gd::SerializerElement element;
objectsContainer.SerializeObjectsTo(element);
element.GetChild(0).SetAttribute("name", "Object2");
objectsContainer.UnserializeObjectsFrom(project, element);
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(objectsContainer.HasObjectNamed("Object2"));
REQUIRE(&objectsContainer.GetObject("Object2") ==
&objectsContainer.GetObject(0));
}
SECTION("Empty container and empty names") {
REQUIRE(!objectsContainer.HasObjectNamed("Object1"));
REQUIRE(!objectsContainer.HasObjectNamed(""));
InsertObject(project, objectsContainer, "Object1");
REQUIRE(!objectsContainer.HasObjectNamed(""));
gd::Object &unnamedObject = InsertObject(project, objectsContainer, "");
REQUIRE(&objectsContainer.GetObject("") == &unnamedObject);
}
SECTION("Names with non-ASCII characters") {
gd::Object &object = InsertObject(project, objectsContainer, "Objet_é_日本");
REQUIRE(objectsContainer.HasObjectNamed("Objet_é_日本"));
REQUIRE(&objectsContainer.GetObject("Objet_é_日本") == &object);
REQUIRE(!objectsContainer.HasObjectNamed("Objet_e_日本"));
object.SetName("Objet_è_日本");
REQUIRE(!objectsContainer.HasObjectNamed("Objet_é_日本"));
REQUIRE(&objectsContainer.GetObject("Objet_è_日本") == &object);
}
SECTION("References stay valid while other objects are changed") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
const gd::Object *found = &objectsContainer.GetObject("Object1");
REQUIRE(found == &object1);
// Enough insertions to force reallocations of the underlying vector.
for (int i = 0; i < 100; ++i) {
InsertObject(project, objectsContainer, "Other" + gd::String::From(i));
}
objectsContainer.RemoveObject("Other3");
objectsContainer.MoveObject(0, 50);
objectsContainer.GetObject("Other4").SetName("RenamedOther4");
REQUIRE(&objectsContainer.GetObject("Object1") == found);
REQUIRE(&objectsContainer.GetObject("Other99") ==
&objectsContainer.GetObject(objectsContainer.GetObjectsCount() - 1));
REQUIRE(!objectsContainer.HasObjectNamed("Other3"));
REQUIRE(!objectsContainer.HasObjectNamed("Other4"));
REQUIRE(objectsContainer.HasObjectNamed("RenamedOther4"));
}
SECTION("Duplicated object renamed afterwards (as done by the IDE)") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
// The IDE inserts a copy (which has the same name), then renames it.
gd::Object &duplicatedObject =
objectsContainer.InsertObject(objectsContainer.GetObject("Object1"), 0);
REQUIRE(&objectsContainer.GetObject("Object1") == &duplicatedObject);
duplicatedObject.SetName("Object1Copy");
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
REQUIRE(&objectsContainer.GetObject("Object1Copy") == &duplicatedObject);
}
SECTION("A copied container outlives the original") {
std::unique_ptr<gd::ObjectsContainer> copiedObjectsContainer;
gd::ObjectsContainer assignedObjectsContainer(gd::ObjectsContainer::Global);
InsertObject(project, assignedObjectsContainer, "OldObject");
REQUIRE(assignedObjectsContainer.HasObjectNamed("OldObject"));
{
gd::ObjectsContainer originalObjectsContainer(
gd::ObjectsContainer::Scene);
InsertObject(project, originalObjectsContainer, "Object1");
// Build the index of the original before copying it.
REQUIRE(originalObjectsContainer.HasObjectNamed("Object1"));
copiedObjectsContainer.reset(
new gd::ObjectsContainer(originalObjectsContainer));
assignedObjectsContainer = originalObjectsContainer;
}
REQUIRE(copiedObjectsContainer->HasObjectNamed("Object1"));
REQUIRE(&copiedObjectsContainer->GetObject("Object1") ==
&copiedObjectsContainer->GetObject(0));
REQUIRE(!assignedObjectsContainer.HasObjectNamed("OldObject"));
REQUIRE(&assignedObjectsContainer.GetObject("Object1") ==
&assignedObjectsContainer.GetObject(0));
}
SECTION("Lookups through an ObjectsContainersList follow renames") {
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &globalObject =
InsertObject(project, project.GetObjects(), "GlobalObject");
gd::Object &sceneObject =
InsertObject(project, layout.GetObjects(), "SceneObject");
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
// The variables container identifies which object was found.
auto findVariables = [&](const gd::String &name) {
return objectsContainersList.GetObjectOrGroupVariablesContainer(name);
};
REQUIRE(findVariables("GlobalObject") == &globalObject.GetVariables());
REQUIRE(findVariables("SceneObject") == &sceneObject.GetVariables());
REQUIRE(findVariables("UnknownObject") == nullptr);
REQUIRE(!objectsContainersList.HasObjectNamed("UnknownObject"));
sceneObject.SetName("RenamedSceneObject");
globalObject.SetName("SceneObject");
REQUIRE(!objectsContainersList.HasObjectNamed("GlobalObject"));
REQUIRE(findVariables("RenamedSceneObject") == &sceneObject.GetVariables());
REQUIRE(findVariables("SceneObject") == &globalObject.GetVariables());
layout.GetObjects().RemoveObject("RenamedSceneObject");
REQUIRE(!objectsContainersList.HasObjectNamed("RenamedSceneObject"));
REQUIRE(findVariables("RenamedSceneObject") == nullptr);
}
SECTION("The first object wins when two objects have the same name") {
gd::Object &object1 = InsertObject(project, objectsContainer, "Object1");
gd::Object &object2 = InsertObject(project, objectsContainer, "Object2");
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
object2.SetName("Object1");
REQUIRE(&objectsContainer.GetObject("Object1") == &object1);
objectsContainer.MoveObject(1, 0);
REQUIRE(&objectsContainer.GetObject("Object1") == &object2);
}
}
+126
View File
@@ -825,3 +825,129 @@ TEST_CASE("ObjectContainersList (GetAnimationNamesOfObject)", "[common]") {
REQUIRE(animationNames.size() == 2);
}
}
TEST_CASE("ObjectContainersList (scene group shadowing a global group)",
"[common]") {
// A scene group having the same name as a global group is an inconsistency
// that the editor tries to avoid, but it can exist in projects. In this
// case, the scene group shadows the global one (the global group is
// considered as not existing in this scene), consistently across all the
// helpers - including ExpandObjectName, which is used to generate the code
// of events.
SECTION("An empty scene group shadows a global group having objects") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Object &globalObject = project.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "GlobalObject", 0);
globalObject.AddNewBehavior(project, "MyExtension::MyBehavior",
"MyBehavior");
globalObject.GetVariables().InsertNew("MyVariable", 0);
auto &globalGroup =
project.GetObjects().GetObjectGroups().InsertNew("Group", 0);
globalGroup.AddObject(globalObject.GetName());
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
layout.GetObjects().GetObjectGroups().InsertNew("Group", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("Group") == "");
REQUIRE(objectsContainersList.GetBehaviorsOfObject("Group", true).empty());
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"Group", "MyBehavior", true) == "");
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("Group",
"MyBehavior"));
REQUIRE(objectsContainersList
.GetBehaviorNamesInObjectOrGroup(
"Group", "MyExtension::MyBehavior", true)
.empty());
REQUIRE(objectsContainersList.HasObjectOrGroupWithVariableNamed(
"Group", "MyVariable") ==
gd::ObjectsContainersList::VariableExistence::GroupIsEmpty);
REQUIRE(objectsContainersList.ExpandObjectName("Group", "").empty());
}
SECTION("A scene group having objects shadows a global group") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Object &globalObject = project.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "GlobalObject", 0);
globalObject.AddNewBehavior(project, "MyExtension::MyBehavior",
"MyBehavior");
auto &globalGroup =
project.GetObjects().GetObjectGroups().InsertNew("Group", 0);
globalGroup.AddObject(globalObject.GetName());
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
gd::Object &sceneObject = layout.GetObjects().InsertNewObject(
project, "MyExtension::FakeObjectWithDefaultBehavior", "SceneObject",
0);
sceneObject.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
"MyOtherBehavior");
auto &sceneGroup =
layout.GetObjects().GetObjectGroups().InsertNew("Group", 0);
sceneGroup.AddObject(sceneObject.GetName());
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("Group") ==
"MyExtension::FakeObjectWithDefaultBehavior");
const auto behaviors =
objectsContainersList.GetBehaviorsOfObject("Group", true);
REQUIRE(std::find(behaviors.begin(), behaviors.end(), "MyOtherBehavior") !=
behaviors.end());
REQUIRE(std::find(behaviors.begin(), behaviors.end(), "MyBehavior") ==
behaviors.end());
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"Group", "MyOtherBehavior", true) ==
"MyExtension::MyOtherBehavior");
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"Group", "MyBehavior", true) == "");
REQUIRE(objectsContainersList.HasBehaviorInObjectOrGroup(
"Group", "MyOtherBehavior"));
REQUIRE(!objectsContainersList.HasBehaviorInObjectOrGroup("Group",
"MyBehavior"));
const auto expandedObjects =
objectsContainersList.ExpandObjectName("Group", "");
REQUIRE(expandedObjects.size() == 1);
REQUIRE(expandedObjects[0] == "SceneObject");
}
SECTION("A global group is used when the scene has no group with its name") {
gd::Platform platform;
gd::Project project;
SetupProjectWithDummyPlatform(project, platform);
gd::Object &globalObject = project.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "GlobalObject", 0);
globalObject.AddNewBehavior(project, "MyExtension::MyBehavior",
"MyBehavior");
auto &globalGroup =
project.GetObjects().GetObjectGroups().InsertNew("Group", 0);
globalGroup.AddObject(globalObject.GetName());
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
auto objectsContainersList = gd::ObjectsContainersList::
MakeNewObjectsContainersListForProjectAndLayout(project, layout);
REQUIRE(objectsContainersList.GetTypeOfObject("Group") ==
"MyExtension::Sprite");
const auto behaviors =
objectsContainersList.GetBehaviorsOfObject("Group", true);
REQUIRE(behaviors.size() == 1);
REQUIRE(behaviors[0] == "MyBehavior");
REQUIRE(objectsContainersList.GetTypeOfBehaviorInObjectOrGroup(
"Group", "MyBehavior", true) == "MyExtension::MyBehavior");
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
/**
* @file Tests covering common features of GDevelop Core.
*/
#include "GDCore/IDE/PropertyFunctionGenerator.h"
#include "GDCore/IDE/ExtensionEditor/PropertyFunctionGenerator.h"
#include "DummyPlatform.h"
#include "GDCore/Events/Builtin/StandardEvent.h"
#include "GDCore/Extensions/Metadata/ValueTypeMetadata.h"
+45
View File
@@ -12,6 +12,7 @@
#include "GDCore/CommonTools.h"
#include "GDCore/Project/VariablesContainer.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "catch.hpp"
TEST_CASE("Variable", "[common][variables]") {
@@ -171,4 +172,48 @@ TEST_CASE("Variable", "[common][variables]") {
REQUIRE(variable.GetChild("MyChild").GetPersistentUuid() == childUuid);
REQUIRE(variable.GetChild("MyNewChild").GetPersistentUuid() != "");
}
SECTION("Serialization keeps the editor-only \"mixed values\" marker") {
// The editor merges the variables of the objects of a group into a
// temporary container, marking the variables that don't have the same
// value (or type) on all objects as having "mixed values". This container
// is copied and snapshotted through serialization, so the marker must
// survive a serialization round trip - including for the variables that
// have no value and no children to read (a structure marked as having
// mixed values, as the marker clears its children, or a variable with
// entirely mixed types).
gd::Variable numberVariable;
numberVariable.SetValue(123);
numberVariable.MarkAsMixedValues();
gd::Variable structureVariable;
structureVariable.GetChild("MyChild").SetValue(456);
structureVariable.MarkAsMixedValues();
gd::Variable mixedTypesVariable;
mixedTypesVariable.CastTo(gd::Variable::Type::MixedTypes);
gd::SerializerElement element;
numberVariable.SerializeTo(element.AddChild("number"));
structureVariable.SerializeTo(element.AddChild("structure"));
mixedTypesVariable.SerializeTo(element.AddChild("mixedTypes"));
gd::Variable unserializedNumberVariable;
unserializedNumberVariable.UnserializeFrom(element.GetChild("number"));
REQUIRE(unserializedNumberVariable.GetType() == gd::Variable::Type::Number);
REQUIRE(unserializedNumberVariable.HasMixedValues());
gd::Variable unserializedStructureVariable;
unserializedStructureVariable.UnserializeFrom(
element.GetChild("structure"));
REQUIRE(unserializedStructureVariable.GetType() ==
gd::Variable::Type::Structure);
REQUIRE(unserializedStructureVariable.HasMixedValues());
gd::Variable unserializedMixedTypesVariable;
unserializedMixedTypesVariable.UnserializeFrom(
element.GetChild("mixedTypes"));
REQUIRE(unserializedMixedTypesVariable.GetType() ==
gd::Variable::Type::MixedTypes);
REQUIRE(unserializedMixedTypesVariable.HasMixedValues());
}
}
+439 -35
View File
@@ -1583,6 +1583,31 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
"RenamedObjectWithMyBehavior.GetObjectNumber() + RenamedObjectWithMyBehavior.MyVariable + RenamedObjectWithMyBehavior.MyStructureVariable.Child");
}
}
SECTION("Global object in scene events") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &layout = project.InsertNewLayout("Scene", 0);
auto &globalObject = project.GetObjects().InsertNewObject(
project, "MyExtension::Sprite", "GlobalObject", 0);
const auto &objectParameterInstruction =
CreateInstructionWithObjectParameter(
project, layout.GetEvents(), globalObject.GetName());
const auto &objectExpressionInstruction =
CreateInstructionWithNumberParameter(
project, layout.GetEvents(), "GlobalObject.GetObjectNumber()");
gd::WholeProjectRefactorer::GlobalObjectOrGroupRenamed(
project, "GlobalObject", "RenamedGlobalObject",
/* isObjectGroup=*/false);
REQUIRE(objectParameterInstruction.GetParameter(0).GetPlainString() ==
"RenamedGlobalObject");
REQUIRE(objectExpressionInstruction.GetParameter(0).GetPlainString() ==
"RenamedGlobalObject.GetObjectNumber()");
}
}
SECTION("Group renamed (in layout)") {
@@ -2457,6 +2482,77 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
}
}
SECTION("Free function moved (instructions update)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsFunction(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsFunction",
"MyRenamedEventsFunction");
for (auto *eventsList : GetEventsLists(project)) {
// Check that events function calls in instructions have been renamed
REQUIRE(
GetEventFirstActionType(eventsList->GetEvent(FreeFunctionAction)) ==
"MyOtherEventsExtension::MyRenamedEventsFunction");
}
}
SECTION("Free function moved (expressions updated)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsFunction(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsFunctionExpression",
"MyRenamedFunctionExpression");
for (auto *eventsList : GetEventsLists(project)) {
// Check that events function calls in expressions have been renamed
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(FreeFunctionWithExpression)) ==
"1 + MyOtherEventsExtension::MyRenamedFunctionExpression(123, 456)");
}
}
SECTION("Free function moved (expression and condition updated)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsFunction(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsFunctionExpressionAndCondition",
"MyRenamedFunctionExpressionAndCondition");
for (auto *eventsList : GetEventsLists(project)) {
// Check that events function calls in expressions have been renamed
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(FreeExpressionFromExpressionAndCondition)) ==
"2 + MyOtherEventsExtension::MyRenamedFunctionExpressionAndCondition(111, 222)");
// Check that events function calls in instructions have been renamed
REQUIRE(GetEventFirstConditionType(
eventsList->GetEvent(FreeConditionFromExpressionAndCondition)) ==
"MyOtherEventsExtension::MyRenamedFunctionExpressionAndCondition");
// Check that the action still refer to the right ExpressionAndCondition.
REQUIRE(eventsExtension.GetEventsFunctions()
.GetEventsFunction("MyEventsFunctionActionWithOperator")
.GetGetterName() ==
"MyRenamedFunctionExpressionAndCondition");
}
}
SECTION("(Free function) number parameter renamed (in expressions)") {
gd::Project project;
gd::Platform platform;
@@ -3056,6 +3152,223 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
}
}
// TODO: Check that this works when behaviors are attached to a child-object.
SECTION("Events based behavior renamed (other behaviors properties update)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().Get("MyEventsBasedBehavior");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
// Set up another events based behavior having a "required behavior"
// property referring to the behavior.
auto &otherEventsExtension =
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
auto &otherEventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().InsertNew(
"MyOtherEventsBasedBehavior");
auto &otherEventsBasedBehaviorFirstProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("MyEventsExtension::MyEventsBasedBehavior");
// Also add another "required behavior" property referring to another
// unrelated behavior.
auto &otherEventsBasedBehaviorSecondProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("SomeOtherExtension::SomeOtherBehavior");
gd::WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsBasedBehavior", "MyRenamedEventsBasedBehavior");
// Check the other events-based behavior has its property updated.
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().at(0) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
// Check the other events-based behavior has its other property left
// untouched.
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().at(0) ==
"SomeOtherExtension::SomeOtherBehavior");
}
SECTION("Events based behavior moved (instructions update)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().Get("MyEventsBasedBehavior");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsBasedBehavior", "MyRenamedEventsBasedBehavior");
// Check that the type of the behavior was changed in the behaviors of
// objects. Name is *not* changed.
REQUIRE(project.GetLayout("Scene")
.GetObjects()
.GetObject("ObjectWithMyBehavior")
.GetBehavior("MyBehavior")
.GetTypeName() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
REQUIRE(project.GetObjects()
.GetObject("GlobalObjectWithMyBehavior")
.GetBehavior("MyBehavior")
.GetTypeName() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
REQUIRE(project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedObjects()
.Get("MyOtherEventsBasedObject")
.GetObjects()
.GetObject("ObjectWithMyBehavior")
.GetBehavior("MyBehavior")
.GetTypeName() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
for (auto *eventsList : GetEventsLists(project)) {
// Check if events-based behavior methods have been renamed in
// instructions
REQUIRE(GetEventFirstActionType(eventsList->GetEvent(BehaviorAction)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior::"
"MyBehaviorEventsFunction");
REQUIRE(GetEventFirstConditionType(
eventsList->GetEvent(BehaviorConditionFromExpressionAndCondition)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior::"
"MyBehaviorEventsFunctionExpressionAndCondition");
REQUIRE(GetEventFirstActionType(
eventsList->GetEvent(BehaviorActionWithOperator)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior::"
"MyBehaviorEventsFunctionActionWithOperator");
// Check if events-based behaviors properties have been renamed in
// instructions
REQUIRE(GetEventFirstActionType(
eventsList->GetEvent(BehaviorPropertyAction)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior::"
"SetPropertyMyProperty");
REQUIRE(GetEventFirstActionType(
eventsList->GetEvent(BehaviorSharedPropertyAction)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior::"
"SetSharedPropertyMySharedProperty");
// Check events-based behavior methods have *not* been renamed in
// expressions
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(BehaviorExpression)) ==
"1 + ObjectWithMyBehavior.MyBehavior::"
"MyBehaviorEventsFunctionExpression(123, 456, 789)");
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(BehaviorExpressionFromExpressionAndCondition)) ==
"5 + ObjectWithMyBehavior.MyBehavior::"
"MyBehaviorEventsFunctionExpressionAndCondition(111, 222)");
}
}
SECTION("Events based behavior moved (in parameters)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().Get("MyEventsBasedBehavior");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsBasedBehavior", "MyRenamedEventsBasedBehavior");
// Free function
auto &myEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsFunctions()
.GetEventsFunction("MyEventsFunction");
REQUIRE(myEventsFunction.GetParameters().GetParameter(2).GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
// Behavior function
{
auto &myBehaviorEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedBehaviors()
.Get("MyEventsBasedBehavior")
.GetEventsFunctions()
.GetEventsFunction("MyBehaviorEventsFunction");
REQUIRE(myBehaviorEventsFunction.GetParameters()
.GetParameter(3)
.GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
}
// Object function
{
auto &myBehaviorEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedObjects()
.Get("MyEventsBasedObject")
.GetEventsFunctions()
.GetEventsFunction("MyObjectEventsFunction");
REQUIRE(myBehaviorEventsFunction.GetParameters()
.GetParameter(2)
.GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
}
}
// TODO: Check that this works when behaviors are attached to a child-object.
SECTION("Events based behavior moved (other behaviors properties update)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().Get("MyEventsBasedBehavior");
// Set up another events based behavior having a "required behavior"
// property referring to the behavior.
auto &otherEventsExtension =
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
auto &otherEventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().InsertNew(
"MyOtherEventsBasedBehavior");
auto &otherEventsBasedBehaviorFirstProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("MyEventsExtension::MyEventsBasedBehavior");
// Also add another "required behavior" property referring to another
// unrelated behavior.
auto &otherEventsBasedBehaviorSecondProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("SomeOtherExtension::SomeOtherBehavior");
gd::WholeProjectRefactorer::MoveEventsBasedBehavior(
project, eventsExtension, "MyEventsExtension", "MyOtherEventsExtension",
"MyEventsBasedBehavior", "MyRenamedEventsBasedBehavior");
// Check the other events-based behavior has its property updated.
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().at(0) ==
"MyOtherEventsExtension::MyRenamedEventsBasedBehavior");
// Check the other events-based behavior has its other property left
// untouched.
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().at(0) ==
"SomeOtherExtension::SomeOtherBehavior");
}
SECTION("Events based object renamed (instructions update)") {
gd::Project project;
gd::Platform platform;
@@ -3185,50 +3498,141 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
.GetObjectType() ==
"MyEventsExtension::MyRenamedEventsBasedObject");
}
// TODO: Check that this works when behaviors are attached to a child-object.
SECTION("Events based behavior renamed (other behaviors properties update)") {
SECTION("Events based object moved (instructions update)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().Get("MyEventsBasedBehavior");
auto &eventsBasedObject =
eventsExtension.GetEventsBasedObjects().Get("MyEventsBasedObject");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
// Set up another events based behavior having a "required behavior"
// property referring to the behavior.
auto &otherEventsExtension =
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
auto &otherEventsBasedBehavior =
eventsExtension.GetEventsBasedBehaviors().InsertNew(
"MyOtherEventsBasedBehavior");
auto &otherEventsBasedBehaviorFirstProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("MyEventsExtension::MyEventsBasedBehavior");
gd::WholeProjectRefactorer::MoveEventsBasedObject(
project, eventsExtension, "MyEventsExtension",
"MyOtherEventsExtension", "MyEventsBasedObject",
"MyRenamedEventsBasedObject");
// Also add another "required behavior" property referring to another
// unrelated behavior.
auto &otherEventsBasedBehaviorSecondProperty =
otherEventsBasedBehavior.GetPropertyDescriptors()
.InsertNew("SomeRequiredBehavior")
.SetType("Behavior")
.AddExtraInfo("SomeOtherExtension::SomeOtherBehavior");
// Check that the type of the object was changed. Name is *not* changed.
REQUIRE(project.GetLayout("Scene")
.GetObjects()
.GetObject("MyCustomObject")
.GetType() == "MyOtherEventsExtension::MyRenamedEventsBasedObject");
REQUIRE(project.GetObjects()
.GetObject("MyGlobalCustomObject")
.GetType() == "MyOtherEventsExtension::MyRenamedEventsBasedObject");
REQUIRE(project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedObjects()
.Get("MyOtherEventsBasedObject")
.GetObjects()
.GetObject("MyCustomObject")
.GetType() == "MyOtherEventsExtension::MyRenamedEventsBasedObject");
gd::WholeProjectRefactorer::RenameEventsBasedBehavior(
project, eventsExtension, "MyEventsBasedBehavior",
"MyRenamedEventsBasedBehavior");
for (auto *eventsList : GetEventsLists(project)) {
// Check if events-based object methods have been renamed in
// instructions
REQUIRE(GetEventFirstActionType(eventsList->GetEvent(ObjectAction)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject::"
"MyObjectEventsFunction");
REQUIRE(GetEventFirstConditionType(
eventsList->GetEvent(ObjectConditionFromExpressionAndCondition)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject::"
"MyObjectEventsFunctionExpressionAndCondition");
REQUIRE(GetEventFirstActionType(
eventsList->GetEvent(ObjectActionWithOperator)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject::"
"MyObjectEventsFunctionActionWithOperator");
// Check the other events-based behavior has its property updated.
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorFirstProperty.GetExtraInfo().at(0) ==
"MyEventsExtension::MyRenamedEventsBasedBehavior");
// Check if events-based object properties have been renamed in
// instructions
REQUIRE(
GetEventFirstActionType(eventsList->GetEvent(ObjectPropertyAction)) ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject::"
"SetPropertyMyProperty");
// Check the other events-based behavior has its other property left
// untouched.
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().size() == 1);
REQUIRE(otherEventsBasedBehaviorSecondProperty.GetExtraInfo().at(0) ==
"SomeOtherExtension::SomeOtherBehavior");
// Check events-based object methods have *not* been renamed in
// expressions
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(ObjectExpression)) ==
"1 + MyCustomObject."
"MyObjectEventsFunctionExpression(123, 456, 789)");
REQUIRE(GetEventFirstActionFirstParameterString(
eventsList->GetEvent(ObjectExpressionFromExpressionAndCondition)) ==
"5 + MyCustomObject."
"MyObjectEventsFunctionExpressionAndCondition(111, 222)");
}
}
SECTION("Events based object moved (in parameters)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedObject =
eventsExtension.GetEventsBasedObjects().Get("MyEventsBasedObject");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsBasedObject(
project, eventsExtension, "MyEventsExtension",
"MyOtherEventsExtension", "MyEventsBasedObject",
"MyRenamedEventsBasedObject");
// Free function
auto &myEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsFunctions()
.GetEventsFunction("MyEventsFunction");
REQUIRE(myEventsFunction.GetParameters().GetParameter(1).GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject");
// Behavior function
{
auto &myBehaviorEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedBehaviors()
.Get("MyEventsBasedBehavior")
.GetEventsFunctions()
.GetEventsFunction("MyBehaviorEventsFunction");
REQUIRE(myBehaviorEventsFunction.GetParameters()
.GetParameter(2)
.GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject");
}
// Object function
{
auto &myBehaviorEventsFunction =
project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedObjects()
.Get("MyEventsBasedObject")
.GetEventsFunctions()
.GetEventsFunction("MyObjectEventsFunction");
REQUIRE(myBehaviorEventsFunction.GetParameters()
.GetParameter(1)
.GetExtraInfo() ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject");
}
}
SECTION("Events based object moved (in behavior object type)") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
auto &eventsExtension = SetupProjectWithEventsFunctionExtension(project);
auto &eventsBasedObject =
eventsExtension.GetEventsBasedObjects().Get("MyEventsBasedObject");
project.InsertNewEventsFunctionsExtension("MyOtherEventsExtension", 0);
gd::WholeProjectRefactorer::MoveEventsBasedObject(
project, eventsExtension, "MyEventsExtension",
"MyOtherEventsExtension", "MyEventsBasedObject",
"MyRenamedEventsBasedObject");
REQUIRE(project.GetEventsFunctionsExtension("MyEventsExtension")
.GetEventsBasedBehaviors()
.Get("MyEventsBasedBehavior")
.GetObjectType() ==
"MyOtherEventsExtension::MyRenamedEventsBasedObject");
}
SECTION("(Events based behavior) events action renamed") {
+3 -1
View File
@@ -19,7 +19,9 @@ namespace gdjs {
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
THREE_ADDONS.BrightnessContrastShader
gdjs.PixiFiltersTools.clampThreeShaderOutput(
THREE_ADDONS.BrightnessContrastShader
)
);
this._isEnabled = false;
}
+3 -1
View File
@@ -19,7 +19,9 @@ namespace gdjs {
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
THREE_ADDONS.HueSaturationShader
gdjs.PixiFiltersTools.clampThreeShaderOutput(
THREE_ADDONS.HueSaturationShader
)
);
this._isEnabled = false;
}
@@ -72,6 +72,9 @@ namespace gdjs {
this.updateTexture();
this._center = new Float32Array([runtimeObject.x, runtimeObject.y]);
this._defaultVertexBuffer = new Float32Array(8);
// The object's angle is not applied yet at construction time (it is set
// afterwards, via setAngle), so the initial quad is the axis-aligned
// square. _updateBuffers rebuilds it with the rotation applied.
this._vertexBuffer = new Float32Array([
runtimeObject.x - this._radius,
runtimeObject.y + this._radius,
@@ -326,6 +329,7 @@ namespace gdjs {
center: this._center,
radius: this._radius,
color: this._color,
angle: gdjs.toRad(this._object.getAngle()),
};
if (this._texture) {
// @ts-ignore
@@ -409,21 +413,24 @@ namespace gdjs {
const computedVertices = this._computeLightVertices();
if (!computedVertices.length) {
// Draw the same rotated quad the mesh falls back to when there are
// no obstacles around (see _updateBuffers), so the debug overlay
// matches the actual light boundary. _updateBuffers recomputes the
// same values into this buffer right after, so reusing it is safe.
const corners = this._defaultVertexBuffer;
this._computeRotatedSquareVertices(
gdjs.toRad(this._object.getAngle()),
corners
);
debugGraphics.clear();
debugGraphics
.lineStyle(1, 16711680, 1)
.moveTo(this._object.x, this._object.y)
.lineTo(this._object.x - this._radius, this._object.y + this._radius)
.lineTo(this._object.x + this._radius, this._object.y + this._radius)
.moveTo(this._object.x, this._object.y)
.lineTo(this._object.x + this._radius, this._object.y + this._radius)
.lineTo(this._object.x + this._radius, this._object.y - this._radius)
.moveTo(this._object.x, this._object.y)
.lineTo(this._object.x + this._radius, this._object.y - this._radius)
.lineTo(this._object.x - this._radius, this._object.y - this._radius)
.moveTo(this._object.x, this._object.y)
.lineTo(this._object.x - this._radius, this._object.y - this._radius)
.lineTo(this._object.x - this._radius, this._object.y + this._radius);
debugGraphics.lineStyle(1, 16711680, 1);
for (let i = 0; i < 8; i += 2) {
const next = (i + 2) % 8;
debugGraphics
.moveTo(this._object.x, this._object.y)
.lineTo(corners[i], corners[i + 1])
.lineTo(corners[next], corners[next + 1]);
}
return;
}
const vertices = new Array(2 * computedVertices.length + 2);
@@ -451,24 +458,46 @@ namespace gdjs {
}
}
/**
* Fill the given buffer (4 corners, as x/y pairs) with the corners of the
* light's square *rotated by the given angle* — the same rotated square
* the shader samples the texture from (see texturedFragmentShader). At
* angle 0, this is exactly the axis-aligned square
* [x-radius, x+radius] x [y-radius, y+radius].
*/
_computeRotatedSquareVertices(angleRad: float, output: Float32Array) {
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
const cx = this._object.x;
const cy = this._object.y;
const a = this._radius * (cosA + sinA);
const b = this._radius * (cosA - sinA);
output[0] = cx - a;
output[1] = cy + b;
output[2] = cx + b;
output[3] = cy + a;
output[4] = cx + a;
output[5] = cy - b;
output[6] = cx - b;
output[7] = cy - a;
}
_updateBuffers() {
if (!this._light) {
return;
}
this._center[0] = this._object.x;
this._center[1] = this._object.y;
const angleRad = gdjs.toRad(this._object.getAngle());
this._light.shader.uniforms.angle = angleRad;
const vertices = this._computeLightVertices();
// Fallback to simple quad when there are no obstacles around.
// Fallback to simple quad when there are no obstacles around: the
// light's rotated square, so it exactly matches the area the shader
// samples the texture from, without clipping its corners nor
// over-sizing the mesh.
if (vertices.length === 0) {
this._defaultVertexBuffer[0] = this._object.x - this._radius;
this._defaultVertexBuffer[1] = this._object.y + this._radius;
this._defaultVertexBuffer[2] = this._object.x + this._radius;
this._defaultVertexBuffer[3] = this._object.y + this._radius;
this._defaultVertexBuffer[4] = this._object.x + this._radius;
this._defaultVertexBuffer[5] = this._object.y - this._radius;
this._defaultVertexBuffer[6] = this._object.x - this._radius;
this._defaultVertexBuffer[7] = this._object.y - this._radius;
this._computeRotatedSquareVertices(angleRad, this._defaultVertexBuffer);
this._light.shader.uniforms.center = this._center;
this._light.geometry
.getBuffer('aVertexPosition')
@@ -546,18 +575,35 @@ namespace gdjs {
* @returns the vertices of mesh.
*/
_computeLightVertices(): Array<FloatPoint> {
// An untextured light renders as a circle (see defaultFragmentShader),
// which never lights anything beyond ±radius whatever the angle, so
// the ±radius box is an exact obstacle search area for it.
// A textured light's lit region is the light's square *rotated by the
// object's angle* (see the fallback quad in _updateBuffers and
// texturedFragmentShader), whose axis-aligned bounding box extends up
// to radius * (|cos| + |sin|) from the center. Search obstacles in
// that whole box — searching only ±radius would miss obstacles near
// the rotated square's corners and let the light shine through them.
// At angle 0 both are exactly ±radius, as before rotation support.
let searchHalfExtent = this._radius;
if (this._texture !== null) {
const angleRad = gdjs.toRad(this._object.getAngle());
searchHalfExtent =
this._radius *
(Math.abs(Math.cos(angleRad)) + Math.abs(Math.sin(angleRad)));
}
const lightObstacles = this._lightObstaclesTemp;
if (this._manager) {
this._manager.getAllObstaclesAround(
this._object,
this._radius,
searchHalfExtent,
lightObstacles
);
}
const searchAreaLeft = this._object.getX() - this._radius;
const searchAreaTop = this._object.getY() - this._radius;
const searchAreaRight = this._object.getX() + this._radius;
const searchAreaBottom = this._object.getY() + this._radius;
const searchAreaLeft = this._object.getX() - searchHalfExtent;
const searchAreaTop = this._object.getY() - searchHalfExtent;
const searchAreaRight = this._object.getX() + searchHalfExtent;
const searchAreaBottom = this._object.getY() + searchHalfExtent;
// Bail out early if there are no obstacles.
if (lightObstacles.length === 0) {
@@ -590,10 +636,16 @@ namespace gdjs {
}
}
let maxX = this._object.x + this._radius;
let minX = this._object.x - this._radius;
let maxY = this._object.y + this._radius;
let minY = this._object.y - this._radius;
// Seed the self-boundary ("wall" that unoccluded rays terminate on)
// with the axis-aligned bounding box of the light's rotated square —
// the same box used to search obstacles above. At angle 0 this is
// exactly [x-radius, x+radius] x [y-radius, y+radius], same as before;
// at other angles it grows just enough to avoid clipping the corners
// of the rotated texture, without over-sizing the search area.
let minX = searchAreaLeft;
let maxX = searchAreaRight;
let minY = searchAreaTop;
let maxY = searchAreaBottom;
const flattenVertices = this._flattenVerticesTemp;
flattenVertices.length = 0;
for (let i = 1; i < obstaclePolygons.length; i++) {
@@ -766,9 +818,17 @@ namespace gdjs {
void main() {
float l = length(vPos - center);
float intensity = 0.0;
if(l < radius)
intensity = clamp((radius - l)*(radius - l)/(radius*radius), 0.0, 1.0);
// Discard fragments outside the light's radius rather than writing a
// transparent color. PixiJS's stencil-based masking (used when this
// mesh is assigned as another object's mask, e.g. via an object
// masking extension) only looks at which fragments were rasterized,
// not their color/alpha - a plain color write of (0,0,0,1) would
// still count as "inside" the mask, making the light's whole
// quad/mesh footprint act as the mask instead of its actual visible
// circular shape. discard removes the fragment from the pipeline
// entirely, so only the true circle is considered "inside".
if (l >= radius) discard;
float intensity = clamp((radius - l)*(radius - l)/(radius*radius), 0.0, 1.0);
gl_FragColor = vec4(color*intensity, 1.0);
}`;
static texturedFragmentShader = `
@@ -776,15 +836,36 @@ namespace gdjs {
uniform vec2 center;
uniform float radius;
uniform vec3 color;
uniform float angle;
uniform sampler2D uSampler;
varying vec2 vPos;
void main() {
vec2 topleft = vec2(center.x - radius, center.y - radius);
vec2 texCoord = (vPos - topleft)/(2.0 * radius);
gl_FragColor = (texCoord.x > 0.0 && texCoord.x < 1.0 && texCoord.y > 0.0 && texCoord.y < 1.0)
? vec4(color, 1.0) * texture2D(uSampler, texCoord)
: vec4(0.0, 0.0, 0.0, 0.0);
vec2 relativePos = vPos - center;
float cosA = cos(angle);
float sinA = sin(angle);
// Rotate the sampled point back by the object's angle so that the
// texture appears to rotate together with the light object.
vec2 rotatedPos = vec2(
relativePos.x * cosA + relativePos.y * sinA,
-relativePos.x * sinA + relativePos.y * cosA
);
vec2 topleft = vec2(-radius, -radius);
vec2 texCoord = (rotatedPos - topleft)/(2.0 * radius);
// See the comment in defaultFragmentShader above: discard (rather than
// just writing a transparent color) any fragment outside the texture's
// square, and any fragment whose texture pixel is itself fully
// transparent, so stencil-based masking respects the texture's actual
// (rotated) silhouette instead of the mesh's raw quad.
if (texCoord.x <= 0.0 || texCoord.x >= 1.0 || texCoord.y <= 0.0 || texCoord.y >= 1.0) {
discard;
}
vec4 texColor = texture2D(uSampler, texCoord);
if (texColor.a <= 0.0) {
discard;
}
gl_FragColor = vec4(color, 1.0) * texColor;
}`;
}
@@ -6,9 +6,10 @@
* Utility function for adding light object for tests.
* @param {gdjs.RuntimeScene} runtimeScene
* @param {number} radius
* @param {string} [texture] Texture resource name ('' for an untextured light).
* @returns {gdjs.LightRuntimeObject}
*/
const addLightObject = (runtimeScene, radius) => {
const addLightObject = (runtimeScene, radius, texture = '') => {
const lightObj = new gdjs.LightRuntimeObject(runtimeScene, {
name: 'lightObject',
type: 'Lighting::LightObject',
@@ -18,7 +19,7 @@ const addLightObject = (runtimeScene, radius) => {
content: {
radius: radius,
color: '#b4b4b4',
texture: '',
texture: texture,
debugMode: false,
},
});
@@ -193,4 +194,44 @@ describe('Light with obstacles around it', function () {
expect(indexBuffer[index]).to.be(val);
});
});
it('Rotated untextured light ignores obstacles beyond its circular radius.', function () {
// An untextured light renders as a circle that never lights anything
// beyond ±radius whatever the angle, so an obstacle outside the
// ±radius box ([100, 300] x [100, 300]) must not trigger raycasting.
light.setPosition(200, 200);
light.setAngle(45);
obstacle.setPosition(320, 175);
runtimeScene.renderAndStep(1000 / 60);
light.update();
expect(light._renderer._computeLightVertices().length).to.be(0);
});
it('Rotated textured light is occluded by an obstacle beyond the unrotated radius box.', function () {
// A textured light's lit area is the light's square rotated by the
// object's angle: at 45°, it extends up to radius * sqrt(2) ≈ 141.42
// from the center along the axes. Place the obstacle outside the
// unrotated ±radius box ([100, 300] x [100, 300]) but inside the lit
// area: it must still be found and occlude the light.
const texturedLight = addLightObject(runtimeScene, 100, 'texture.png');
texturedLight.setPosition(200, 200);
texturedLight.setAngle(45);
obstacle.setPosition(320, 175);
runtimeScene.renderAndStep(1000 / 60);
texturedLight.update();
const vertices = texturedLight._renderer._computeLightVertices();
expect(vertices.length).to.be.greaterThan(0);
// Rays cast towards the obstacle stop on its left edge (x = 320)
// instead of reaching the boundary of the lit area.
expect(
vertices.some(
(vertex) =>
Math.abs(vertex[0] - 320) < 0.5 &&
vertex[1] >= 175 &&
vertex[1] <= 225
)
).to.be(true);
});
});
@@ -86,6 +86,9 @@ namespace gdjs {
this._manager =
gdjs.NavMeshObstaclesManager.getManager(instanceContainer);
}
override onCreated(): void {
this._manager.addCharacter(this);
}
@@ -28,6 +28,7 @@ namespace gdjs {
* @category Behaviors > NavMesh pathfinding
*/
export class NavMeshObstaclesManager {
instanceContainer: gdjs.RuntimeInstanceContainer;
obstacles = new Set<NavMeshObstacleRuntimeBehavior>();
characters = new Set<NavMeshCharacterRuntimeBehavior>();
is3D = false;
@@ -56,6 +57,7 @@ namespace gdjs {
debuggerRenderer: gdjs.NavMeshDebuggerRenderer | null = null;
constructor(instanceContainer: gdjs.RuntimeInstanceContainer, sharedData) {
this.instanceContainer = instanceContainer;
if (!sharedData) {
// It can happens when there is no object with the character behavior.
return;
@@ -134,7 +136,7 @@ namespace gdjs {
this.addPolygonsFor(object, positions, indices);
}
}
if (!this.is3D && positions.length > 0) {
if (!this.is3D) {
this.addGroundFor2D(positions, indices);
this.navMeshConfig.walkableClimb = 0.1;
this.navMeshConfig.walkableHeight = 2;
@@ -208,17 +210,34 @@ namespace gdjs {
positions: Array<float>,
indices: Array<integer>
): void {
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
for (let index = 0; index + 2 < positions.length; index += 3) {
const x = positions[index];
const y = positions[index + 2];
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
const game = this.instanceContainer.getGame();
let minX = 0;
let minY = 0;
let maxX = game.getGameResolutionWidth();
let maxY = game.getGameResolutionHeight();
if (positions.length > 0) {
minX = Number.POSITIVE_INFINITY;
minY = Number.POSITIVE_INFINITY;
maxX = Number.NEGATIVE_INFINITY;
maxY = Number.NEGATIVE_INFINITY;
for (let index = 0; index + 2 < positions.length; index += 3) {
const x = positions[index];
const y = positions[index + 2];
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
const gameSize = Math.max(
game.getGameResolutionWidth(),
game.getGameResolutionHeight()
);
// Extends the area in case it's not closed by obstacles
// and characters may be expected to go around them.
minX -= gameSize;
minY -= gameSize;
maxX += gameSize;
maxY += gameSize;
}
const width = maxX - minX;
const height = (maxY - minY) * this.inverseSpeedScaleY;
@@ -112,6 +112,7 @@ describe('gdjs.NavMeshCharacterRuntimeBehavior', function () {
return 20;
};
runtimeScene.addObject(character);
character.onCreated();
return character;
};
+78 -19
View File
@@ -219,6 +219,13 @@ module.exports = {
physics2Behavior.getProperties = function (behaviorContent) {
var behaviorProperties = new gd.MapStringPropertyDescriptor();
// The shape decides which dimensions are meaningful and how they
// should be labelled, so these properties adapt themselves to it.
const shape = behaviorContent.getChild('shape').getStringValue();
const isCircleShape = shape === 'Circle';
const isEdgeShape = shape === 'Edge';
const isPolygonShape = shape === 'Polygon';
behaviorProperties
.getOrCreate('bodyType')
.setValue(behaviorContent.getChild('bodyType').getStringValue())
@@ -283,12 +290,18 @@ module.exports = {
.getOrCreate('shape')
.setValue(behaviorContent.getChild('shape').getStringValue())
.setType('Choice')
.setLabel('Shape')
.setLabel(_('Shape'))
.setDescription(
_(
'The shape used for collisions. It gives their meaning to the dimension properties. A polygon is defined by its "Vertices" and positioned according to "Polygon origin".'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.addChoice('Box', _('Box'))
.addChoice('Circle', _('Circle'))
.addChoice('Edge', _('Edge'))
.addChoice('Polygon', _('Polygon'));
.addChoice('Polygon', _('Polygon'))
.setHasImpactOnOtherProperties(true);
behaviorProperties
.getOrCreate('shapeDimensionA')
.setValue(
@@ -299,9 +312,17 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension A')
.setLabel(
isCircleShape ? _('Radius') : isEdgeShape ? _('Length') : _('Width')
)
.setDescription(
_(
'Width of the box, radius of the circle or length of the edge. Use 0 to follow the object size.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// A polygon is defined by its vertices instead of dimensions.
.setHidden(isPolygonShape);
behaviorProperties
.getOrCreate('shapeDimensionB')
.setValue(
@@ -311,10 +332,20 @@ module.exports = {
.toString(10)
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension B')
.setMeasurementUnit(
isEdgeShape
? gd.MeasurementUnit.getDegreeAngle()
: gd.MeasurementUnit.getPixel()
)
.setLabel(isEdgeShape ? _('Angle') : _('Height'))
.setDescription(
_(
'Height of the box, or angle of the edge in degrees. Not used by a circle. Use 0 to follow the object height.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// A circle only needs a radius and a polygon uses its vertices.
.setHidden(isPolygonShape || isCircleShape);
behaviorProperties
.getOrCreate('shapeOffsetX')
.setValue(
@@ -322,9 +353,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Offset X')
.setLabel(_('Shape offset X'))
.setDescription(
_(
'Offset of the collision shape relative to the object center, on the X axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
behaviorProperties
.getOrCreate('shapeOffsetY')
.setValue(
@@ -332,9 +368,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Offset Y')
.setLabel(_('Shape offset Y'))
.setDescription(
_(
'Offset of the collision shape relative to the object center, on the Y axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
behaviorProperties
.getOrCreate('polygonOrigin')
.setValue(
@@ -343,12 +384,16 @@ module.exports = {
: 'Center'
)
.setType('Choice')
.setLabel('Polygon Origin')
.setLabel(_('Polygon origin'))
.setDescription(
_('Point of the object the polygon vertices are relative to.')
)
.addChoice('Center', _('Center'))
.addChoice('Origin', _('Origin'))
.addChoice('TopLeft', _('TopLeft'))
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// Only used by the "Polygon" shape.
.setHidden(!isPolygonShape);
behaviorProperties
.getOrCreate('vertices')
.setValue(
@@ -434,17 +479,31 @@ module.exports = {
behaviorProperties
.getOrCreate('layers')
.setValue(behaviorContent.getChild('layers').getIntValue().toString(10))
.setType('Number')
.setLabel('Layers')
.setType('Bitmask')
.addExtraInfo('bitCount=16')
.setLabel(_('Layers'))
.setDescription(
_(
'Layers the object belongs to, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 16 which is 32768.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setGroup(_('Collision filtering'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('masks')
.setValue(behaviorContent.getChild('masks').getIntValue().toString(10))
.setType('Number')
.setLabel('Masks')
.setType('Bitmask')
.addExtraInfo('bitCount=16')
.setLabel(_('Masks'))
.setDescription(
_(
'Layers the object can collide with, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 16 which is 32768.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setGroup(_('Collision filtering'))
.setAdvanced(true);
return behaviorProperties;
};
+118 -35
View File
@@ -285,6 +285,15 @@ module.exports = {
behavior.getProperties = function (behaviorContent) {
const behaviorProperties = new gd.MapStringPropertyDescriptor();
// The shape decides which dimensions are meaningful and how they
// should be labelled, and the body type decides which collision
// layers are used, so these properties adapt themselves to them.
const shape = behaviorContent.getChild('shape').getStringValue();
const isBoxShape = shape === 'Box';
const isMeshShape = shape === 'Mesh';
const isStaticBody =
behaviorContent.getChild('bodyType').getStringValue() === 'Static';
behaviorProperties
.getOrCreate('object3D')
.setValue(behaviorContent.getChild('object3D').getStringValue())
@@ -343,13 +352,19 @@ module.exports = {
.getOrCreate('shape')
.setValue(behaviorContent.getChild('shape').getStringValue())
.setType('Choice')
.setLabel('Shape')
.setLabel(_('Shape'))
.setDescription(
_(
'The shape used for collisions. It gives their meaning to the dimension properties. A capsule or a cylinder is extended along the axis given by "Shape orientation" and a mesh uses the model given by "Simplified 3D model".'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.addChoice('Box', _('Box'))
.addChoice('Capsule', _('Capsule'))
.addChoice('Sphere', _('Sphere'))
.addChoice('Cylinder', _('Cylinder'))
.addChoice('Mesh', _('Mesh (works for Static only)'));
.addChoice('Mesh', _('Mesh (works for Static only)'))
.setHasImpactOnOtherProperties(true);
behaviorProperties
.getOrCreate('meshShapeResourceName')
.setValue(
@@ -359,8 +374,8 @@ module.exports = {
.addExtraInfo('model3D')
.setLabel(_('Simplified 3D model'))
.setDescription(_("Leave empty to use object's one"))
// Hidden as required to be changed in the full editor.
.setHidden(true)
// Only used by the "Mesh" shape.
.setHidden(!isMeshShape)
.setHasImpactOnOtherProperties(true);
behaviorProperties
.getOrCreate('shapeOrientation')
@@ -368,11 +383,16 @@ module.exports = {
behaviorContent.getChild('shapeOrientation').getStringValue()
)
.setType('Choice')
.setLabel('Shape orientation')
.setLabel(_('Shape orientation'))
.setDescription(
_('Axis along which the capsule or cylinder is extended.')
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.addChoice('Z', _('Z'))
.addChoice('Y', _('Y'))
.addChoice('X', _('X'));
.addChoice('X', _('X'))
// Only capsules and cylinders can be oriented.
.setHidden(shape !== 'Capsule' && shape !== 'Cylinder');
behaviorProperties
.getOrCreate('shapeDimensionA')
.setValue(
@@ -383,9 +403,15 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension A')
.setLabel(isBoxShape ? _('Width') : _('Radius'))
.setDescription(
_(
'Width of the box, or radius of the sphere, capsule or cylinder. Use 0 to follow the object size.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// The "Mesh" shape uses a 3D model instead of dimensions.
.setHidden(isMeshShape);
behaviorProperties
.getOrCreate('shapeDimensionB')
.setValue(
@@ -396,9 +422,15 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension B')
.setLabel(isBoxShape ? _('Height') : _('Depth'))
.setDescription(
_(
'Height of the box, or size of the capsule or cylinder along its orientation axis. Not used by a sphere. Use 0 to follow the object size.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// A sphere only needs a radius and a mesh uses a 3D model.
.setHidden(isMeshShape || shape === 'Sphere');
behaviorProperties
.getOrCreate('shapeDimensionC')
.setValue(
@@ -409,9 +441,15 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension C')
.setLabel(_('Depth'))
.setDescription(
_(
'Depth of the box. Only used by a box. Use 0 to follow the object depth.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
// Only a box has a third dimension to configure.
.setHidden(!isBoxShape);
if (!behaviorContent.hasChild('shapeOffsetX')) {
behaviorContent.addChild('shapeOffsetX').setDoubleValue(0);
}
@@ -425,10 +463,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape offset X')
.setLabel(_('Shape offset X'))
.setDescription(
_(
'Offset of the collision shape relative to the object center, on the X axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setAdvanced(true)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
if (!behaviorContent.hasChild('shapeOffsetY')) {
behaviorContent.addChild('shapeOffsetY').setDoubleValue(0);
}
@@ -442,9 +484,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape offset Y')
.setLabel(_('Shape offset Y'))
.setDescription(
_(
'Offset of the collision shape relative to the object center, on the Y axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
if (!behaviorContent.hasChild('shapeOffsetZ')) {
behaviorContent.addChild('shapeOffsetZ').setDoubleValue(0);
}
@@ -458,10 +505,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape offset Z')
.setLabel(_('Shape offset Z'))
.setDescription(
_(
'Offset of the collision shape relative to the object center, on the Z axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setAdvanced(true)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
if (!behaviorContent.hasChild('massCenterOffsetX')) {
behaviorContent.addChild('massCenterOffsetX').setDoubleValue(0);
}
@@ -475,10 +526,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Center of mass offset X')
.setLabel(_('Center of mass X'))
.setDescription(
_(
'Offset of the center of mass relative to the object center, on the X axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setAdvanced(true)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
if (!behaviorContent.hasChild('massCenterOffsetY')) {
behaviorContent.addChild('massCenterOffsetY').setDoubleValue(0);
}
@@ -492,10 +547,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Center of mass offset Y')
.setLabel(_('Center of mass Y'))
.setDescription(
_(
'Offset of the center of mass relative to the object center, on the Y axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setAdvanced(true)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
if (!behaviorContent.hasChild('massCenterOffsetZ')) {
behaviorContent.addChild('massCenterOffsetZ').setDoubleValue(0);
}
@@ -509,10 +568,14 @@ module.exports = {
)
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Center of mass offset Z')
.setLabel(_('Center of mass Z'))
.setDescription(
_(
'Offset of the center of mass relative to the object center, on the Z axis.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setAdvanced(true)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setAdvanced(true);
behaviorProperties
.getOrCreate('density')
.setValue(
@@ -627,19 +690,39 @@ module.exports = {
.setValue(
behaviorContent.getChild('layers').getIntValue().toString(10)
)
.setType('Number')
.setLabel('Layers')
.setType('Bitmask')
// Static objects use the layers 1 to 4 and moving ones the layers
// 5 to 8, so that they never register in the wrong layer group.
.addExtraInfo('bitCount=4')
.addExtraInfo(isStaticBody ? 'firstBit=0' : 'firstBit=4')
.setLabel(_('Layers'))
.setDescription(
_(
'Layers the object belongs to, as a bitmask. Static objects use the layers 1 to 4 (1, 2, 4 and 8) and moving ones the layers 5 to 8 (16, 32, 64 and 128).'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setGroup(_('Collision filtering'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('masks')
.setValue(
behaviorContent.getChild('masks').getIntValue().toString(10)
)
.setType('Number')
.setLabel('Masks')
.setType('Bitmask')
.addExtraInfo('bitCount=8')
.setLabel(_('Masks'))
.setDescription(
_(
'Layers the object can collide with, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 8 which is 128. Not used by static objects, which accept every collision.'
)
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
.setGroup(_('Collision filtering'))
.setAdvanced(true)
// Static objects accept every collision: it's the mask of the moving
// objects that matters.
.setHidden(isStaticBody);
return behaviorProperties;
};
+2 -2
View File
@@ -226,7 +226,7 @@ module.exports = {
'CheckSaveExistsInStorage',
_('Check if a save exists in device storage'),
_(
'Check if a save with the given name exists in the device storage, and store the result (yes/no) in a variable. The check is asynchronous: use the condition "Save existence check completed" to know when the result is available.'
'Check if a save with the given name exists in the device storage, and store the result (true/false) in a variable. The check is asynchronous: use the condition "Save existence check completed" to know when the result is available.'
),
_(
'Check if save _PARAM1_ exists in device storage (result in _PARAM2_)'
@@ -239,7 +239,7 @@ module.exports = {
.addParameter('string', _('Storage name of the save to check'), '', false)
.addParameter(
'variable',
_('Variable where to store the result (yes/no)'),
_('Variable where to store the result (true/false)'),
'',
false
)
@@ -9,6 +9,9 @@ namespace gdjs {
!!attachment &&
attachment instanceof spine.PointAttachment;
/** `renderMeshes` is private in the Spine typings, but it must be wrapped. */
type SpineWithRenderMeshes = { renderMeshes: () => void };
/**
* @category Renderers > Spine
*/
@@ -16,6 +19,9 @@ namespace gdjs {
private _object: gdjs.SpineRuntimeObject;
private _rendererObject: spine.Spine | PIXI.Container;
private _isAnimationComplete = true;
private _areMeshesOutdated = true;
private _areScaledLocalBoundsOutdated = true;
private _scaledLocalBounds = { x: 0, y: 0, width: 0, height: 0 };
/**
* @param runtimeObject The object to render
@@ -29,6 +35,7 @@ namespace gdjs {
this._rendererObject = this.constructRendererObject();
if (isSpine(this._rendererObject)) {
this._rendererObject.autoUpdate = false;
this._cacheMeshesUntilPoseChanges(this._rendererObject);
}
this.updatePosition();
@@ -42,6 +49,44 @@ namespace gdjs {
.addRendererObject(this._rendererObject, runtimeObject.getZOrder());
}
/**
* `Spine.updateTransform` regenerates every mesh of the skeleton, which is
* the most expensive thing a Spine object does. PixiJS calls it both when
* the local bounds are read (to update the AABB) and when the scene graph
* is rendered, but the meshes only depend on the skeleton pose: make the
* extra calls a no-op until the pose changes.
*/
private _cacheMeshesUntilPoseChanges(spineObject: spine.Spine): void {
const spineObjectWithRenderMeshes =
spineObject as unknown as SpineWithRenderMeshes;
const renderMeshes = spineObjectWithRenderMeshes.renderMeshes;
if (typeof renderMeshes !== 'function') {
throw new Error(
'Spine.renderMeshes is missing: the mesh caching of SpineRuntimeObjectPixiRenderer must be updated for this version of the Spine runtime.'
);
}
spineObjectWithRenderMeshes.renderMeshes = () => {
if (!this._areMeshesOutdated) {
return;
}
this._areMeshesOutdated = false;
renderMeshes.call(spineObject);
};
const previousAfterUpdateWorldTransforms =
spineObject.afterUpdateWorldTransforms;
spineObject.afterUpdateWorldTransforms = (object) => {
previousAfterUpdateWorldTransforms(object);
this._areMeshesOutdated = true;
this._areScaledLocalBoundsOutdated = true;
};
}
updateAnimation(timeDelta: float) {
if (!isSpine(this._rendererObject)) {
return;
@@ -53,15 +98,42 @@ namespace gdjs {
return this._rendererObject;
}
getOriginOffset(): PIXI.Point {
if (!isSpine(this._rendererObject)) return new PIXI.Point(0, 0);
/**
* The local bounds of the skeleton, scaled to the object size. Reading them
* walks every mesh of the skeleton, so they are only recomputed when the
* pose or the scale changed. The returned object must not be modified.
*
* The Spine `boundsProvider` is not used for this: none of its
* implementations returns the bounds of the current frame, and setting one
* disables `interactiveChildren` and overwrites `hitArea`.
*/
private _getScaledLocalBounds(): {
x: float;
y: float;
width: float;
height: float;
} {
if (this._areScaledLocalBoundsOutdated) {
this._areScaledLocalBoundsOutdated = false;
const localBounds = this._rendererObject.getLocalBounds(undefined, true);
const localBounds = this._rendererObject.getLocalBounds(
undefined,
true
);
const scaleX = this._rendererObject.scale.x;
const scaleY = this._rendererObject.scale.y;
const scaledLocalBounds = this._scaledLocalBounds;
scaledLocalBounds.x = localBounds.x * scaleX;
scaledLocalBounds.y = localBounds.y * scaleY;
scaledLocalBounds.width = localBounds.width * scaleX;
scaledLocalBounds.height = localBounds.height * scaleY;
}
return new PIXI.Point(
localBounds.x * this._rendererObject.scale.x,
localBounds.y * this._rendererObject.scale.y
);
return this._scaledLocalBounds;
}
getOriginOffset(): { x: float; y: float } {
return this._getScaledLocalBounds();
}
onDestroy(): void {
@@ -83,6 +155,7 @@ namespace gdjs {
this._rendererObject.scale.y = this._object.isFlippedY()
? -scaleY
: scaleY;
this._areScaledLocalBoundsOutdated = true;
}
updatePosition(): void {
@@ -99,31 +172,33 @@ namespace gdjs {
}
getWidth(): float {
return this._rendererObject.width;
return this._getScaledLocalBounds().width;
}
getHeight(): float {
return this._rendererObject.height;
return this._getScaledLocalBounds().height;
}
setWidth(width: float): void {
this._rendererObject.width = width;
this._areScaledLocalBoundsOutdated = true;
}
setHeight(height: float): void {
this._rendererObject.height = height;
this._areScaledLocalBoundsOutdated = true;
}
getUnscaledWidth(): float {
return Math.abs(
(this._rendererObject.width * this._object._originalScale) /
(this.getWidth() * this._object._originalScale) /
this._rendererObject.scale.x
);
}
getUnscaledHeight(): float {
return Math.abs(
(this._rendererObject.height * this._object._originalScale) /
(this.getHeight() * this._object._originalScale) /
this._rendererObject.scale.y
);
}
+1 -1
View File
@@ -1128,7 +1128,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
objectSimpleTileMap
)
.setCategory('General')
.setAssetStoreTag('Tilemap')
.setAssetStoreTag('tilemap')
.setOpenFullEditorLabel(_('Edit tileset and collisions'))
.addDefaultBehavior('TileMap::TileMapBehavior')
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
+53 -53
View File
@@ -83,7 +83,7 @@ module.exports = {
'',
'JsPlatform/Extensions/tween_behavior32.png'
)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('From value'))
.addParameter('expression', _('To value'))
@@ -115,7 +115,7 @@ module.exports = {
.addParameter('expression', _('Initial value'), '', false)
.addParameter('expression', _('Final value'), '', false)
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/TweenBehavior/TweenManager.js')
.addIncludeFile('Extensions/TweenBehavior/tweentools.js')
@@ -142,7 +142,7 @@ module.exports = {
.addParameter('scenevar', _('The variable to tween'), '', false)
.addParameter('expression', _('Final value'), '', false)
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/TweenBehavior/TweenManager.js')
.addIncludeFile('Extensions/TweenBehavior/tweentools.js')
@@ -166,7 +166,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'sceneTween')
.addParameter('scenevar', _('The variable to tween'), '', false)
.addParameter('expression', _('Final value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -192,7 +192,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('From value'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter('yesorno', _('Exponential interpolation'), '', false)
@@ -221,7 +221,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('From value'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter('yesorno', _('Exponential interpolation'), '', false)
@@ -253,7 +253,7 @@ module.exports = {
.addParameter('expression', _('Target Y position'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/TweenBehavior/TweenManager.js')
.addIncludeFile('Extensions/TweenBehavior/tweentools.js')
@@ -276,7 +276,7 @@ module.exports = {
.addParameter('expression', _('Target X position'), '', false)
.addParameter('expression', _('Target Y position'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -303,7 +303,7 @@ module.exports = {
.addParameter('expression', _('Target zoom'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/TweenBehavior/TweenManager.js')
.addIncludeFile('Extensions/TweenBehavior/tweentools.js')
@@ -325,7 +325,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'sceneTween')
.addParameter('expression', _('Target zoom'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -352,7 +352,7 @@ module.exports = {
.addParameter('expression', _('Target rotation (in degrees)'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/TweenBehavior/TweenManager.js')
.addIncludeFile('Extensions/TweenBehavior/tweentools.js')
@@ -374,7 +374,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'sceneTween')
.addParameter('expression', _('Target rotation (in degrees)'), '', false)
.addParameter('layer', _('Layer'), '', true)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -402,7 +402,7 @@ module.exports = {
.addParameter('layer', _('Layer'), '', true)
.addParameter('layerEffectName', _('Effect name'))
.addParameter('layerEffectParameterName', _('Property name'))
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -430,7 +430,7 @@ module.exports = {
.addParameter('layer', _('Layer'), '', true)
.addParameter('layerEffectName', _('Effect name'))
.addParameter('layerEffectParameterName', _('Property name'))
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.getCodeExtraInformation()
@@ -652,7 +652,7 @@ module.exports = {
.addParameter('objectvar', _('Object variable'), '', false)
.addParameter('expression', _('From value'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -686,7 +686,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('objectvar', _('Object variable'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -718,7 +718,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('objectvar', _('Object variable'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -750,7 +750,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('From value'), '', false)
.addParameter('expression', _('To value'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter('yesorno', _('Exponential interpolation'), '', false)
@@ -784,7 +784,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To X'), '', false)
.addParameter('expression', _('To Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -814,7 +814,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To X'), '', false)
.addParameter('expression', _('To Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -847,7 +847,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To X'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -878,7 +878,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To X'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -911,7 +911,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To Z'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -943,7 +943,7 @@ module.exports = {
.addParameter('behavior', _('3D capability'), 'Scene3D::Base3DBehavior')
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To Z'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -974,7 +974,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To width'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1003,7 +1003,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To width'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1034,7 +1034,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To height'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1063,7 +1063,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To height'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1096,7 +1096,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To depth'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1128,7 +1128,7 @@ module.exports = {
.addParameter('behavior', _('3D capability'), 'Scene3D::Base3DBehavior')
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To depth'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1161,7 +1161,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1192,7 +1192,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1223,7 +1223,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1252,7 +1252,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1284,7 +1284,7 @@ module.exports = {
.addParameter('behavior', _('3D capability'), 'Scene3D::Base3DBehavior')
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1316,7 +1316,7 @@ module.exports = {
.addParameter('behavior', _('3D capability'), 'Scene3D::Base3DBehavior')
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To angle (in degrees)'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1350,7 +1350,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale X'), '', false)
.addParameter('expression', _('To scale Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1386,7 +1386,7 @@ module.exports = {
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale X'), '', false)
.addParameter('expression', _('To scale Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1419,7 +1419,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1454,7 +1454,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale X'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1487,7 +1487,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale X'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1522,7 +1522,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1555,7 +1555,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To scale Y'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1590,7 +1590,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To character size'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1621,7 +1621,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To character size'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1654,7 +1654,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To opacity'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1688,7 +1688,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('expression', _('To opacity'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1726,7 +1726,7 @@ module.exports = {
.addParameter('expression', _('To value'), '', false)
.addParameter('objectEffectName', _('Effect name'))
.addParameter('objectEffectParameterName', _('Property name'))
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1764,7 +1764,7 @@ module.exports = {
.addParameter('color', _('To color'), '', false)
.addParameter('objectEffectName', _('Effect name'))
.addParameter('objectEffectParameterName', _('Property name'))
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1797,7 +1797,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('color', _('To color'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1838,7 +1838,7 @@ module.exports = {
.addParameter('behavior', _('Behavior'), 'TweenBehavior', false)
.addParameter('identifier', _('Tween Identifier'), 'objectTween')
.addParameter('color', _('To color'), '', false)
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
@@ -1897,7 +1897,7 @@ module.exports = {
false
)
.setDefaultValue('-1')
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in milliseconds)'), '', false)
.addParameter(
@@ -1944,7 +1944,7 @@ module.exports = {
false
)
.setDefaultValue('-1')
.addParameter('stringWithSelector', _('Easing'), easingChoices, false)
.addParameter('easing', _('Easing'), easingChoices, false)
.setDefaultValue('linear')
.addParameter('expression', _('Duration (in seconds)'), '', false)
.addParameter(
+15 -1
View File
@@ -603,6 +603,7 @@ namespace gdjs {
protected onFinish: () => void;
protected timeSource: TimeSource;
protected isPaused = false;
protected isFinished = false;
protected tweenInformation: TweenInformation;
constructor(
@@ -632,6 +633,13 @@ namespace gdjs {
this.elapsedTime + this.timeSource.getElapsedTime() / 1000,
this.totalDuration
);
// A tween with a duration of 0 (or less) finishes on its first step.
// The tween is not considered finished at creation, so that
// `_updateValue` is called at least once to apply the targeted value
// and call `onFinish`.
if (this.elapsedTime >= this.totalDuration) {
this.isFinished = true;
}
this._updateValue();
}
@@ -643,11 +651,12 @@ namespace gdjs {
}
hasFinished(): boolean {
return this.elapsedTime === this.totalDuration;
return this.isFinished;
}
stop(jumpToDest: boolean): void {
this.elapsedTime = this.totalDuration;
this.isFinished = true;
if (jumpToDest) {
this._updateValue();
}
@@ -662,12 +671,17 @@ namespace gdjs {
}
getProgress(): float {
if (this.totalDuration <= 0) {
// Avoid a division by 0 for tweens with a duration of 0.
return this.isFinished ? 1 : 0;
}
return this.elapsedTime / this.totalDuration;
}
// To be used for network synchronization.
updateElapsedTime(newElapsedTime: float): void {
this.elapsedTime = newElapsedTime;
this.isFinished = newElapsedTime >= this.totalDuration;
}
abstract getNetworkSyncData(): TweenInstanceNetworkSyncData<T>;
@@ -28,6 +28,24 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
});
it('can play a tween with a duration of 0', () => {
camera.setCameraRotation(runtimeScene, 200, '', 0);
tween.tweenCameraRotation2(runtimeScene, 'MyTween', 600, '', 'linear', 0);
// Tween actions don't change the value directly.
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(200);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(true);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(false);
// The tween reaches the end on its first step.
runtimeScene.renderAndStep(1000 / 60);
expect(tween.sceneTweenIsPlaying(runtimeScene, 'MyTween')).to.be(false);
expect(tween.sceneTweenHasFinished(runtimeScene, 'MyTween')).to.be(true);
expect(camera.getCameraRotation(runtimeScene, '', 0)).to.be(600);
expect(tween.getValue(runtimeScene, 'MyTween')).to.be(600);
expect(tween.getProgress(runtimeScene, 'MyTween')).to.be(1);
});
it('can play a tween till the end', () => {
camera.setCameraRotation(runtimeScene, 200, '', 0);
tween.tweenCameraRotation2(
@@ -244,6 +244,42 @@ describe('gdjs.TweenRuntimeBehavior', () => {
expect(object.getX()).to.be(123);
});
it('can play a tween with a duration of 0', () => {
object.setPosition(200, 300);
behavior.addObjectPositionXTween2('MyTween', 600, 'linear', 0, false);
// Tween actions don't change the value directly.
expect(object.getX()).to.be(200);
expect(behavior.getValue('MyTween')).to.be(200);
expect(behavior.isPlaying('MyTween')).to.be(true);
expect(behavior.hasFinished('MyTween')).to.be(false);
// The tween reaches the end on its first step.
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isPlaying('MyTween')).to.be(false);
expect(behavior.hasFinished('MyTween')).to.be(true);
expect(object.getX()).to.be(600);
expect(behavior.getValue('MyTween')).to.be(600);
expect(behavior.getProgress('MyTween')).to.be(1);
// The value is not set to the targeted value over and over
// after the tween is finished.
object.setX(123);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(123);
});
it('can remove the object at the end of a tween with a duration of 0', () => {
object.setPosition(200, 300);
behavior.addObjectPositionXTween2('MyTween', 600, 'linear', 0, true);
expect(runtimeScene.getInstancesCountOnScene('Object')).to.be(1);
runtimeScene.renderAndStep(1000 / 60);
expect(runtimeScene.getInstancesCountOnScene('Object')).to.be(0);
});
it('can remove the object at the end', () => {
object.setPosition(200, 300);
@@ -413,8 +413,7 @@ bool MetadataDeclarationHelper::IsBehaviorLifecycleEventsFunction(
*/
bool MetadataDeclarationHelper::IsObjectLifecycleEventsFunction(
const gd::String &functionName) {
return functionName == "onCreated" || functionName == "doStepPostEvents" ||
functionName == "onDestroy" || functionName == "onHotReloading";
return gd::EventsBasedObject::IsObjectLifecycleEventsFunction(functionName);
}
/**
+9 -5
View File
@@ -109,7 +109,8 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
/*includeCaptureManager*/ false,
/*includeInAppTutorialMessage*/ false,
exportedProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
includesFiles,
resourcesFiles);
// Export files for free function, object and behaviors
for (const auto &includeFile : usedExtensionsResult.GetUsedIncludeFiles()) {
@@ -135,11 +136,14 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
return false;
}
//...and export it
gd::SerializerElement noRuntimeGameOptions;
//...and export it, with the setup options passed to the gdjs.RuntimeGame.
gd::SerializerElement runtimeGameOptions;
runtimeGameOptions.AddChild("runtimeFilesBaseUrl")
.SetStringValue(
ExporterHelper::GetExportedRuntimeFilesBaseUrl(fs, gdjsRoot));
std::vector<gd::InGameEditorResourceMetadata> noInGameEditorResources;
helper.ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
noRuntimeGameOptions, false, noInGameEditorResources);
runtimeGameOptions, false, noInGameEditorResources);
includesFiles.push_back(codeOutputDir + "/data.js");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
@@ -157,7 +161,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
includesFiles,
usedExtensionsResult.GetUsedSourceFiles(),
/*nonRuntimeScriptsCacheBurst=*/0,
"")) {
"gdjs.runtimeGameOptions")) {
gd::LogError(_("Error during export:\n") + lastError);
return false;
}
+42 -5
View File
@@ -193,7 +193,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
/*includeInAppTutorialMessage*/
!options.inAppTutorialMessageInPreview.empty(),
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
includesFiles,
resourcesFiles);
// Export files for free function, object and behaviors
for (const auto &includeFile : usedExtensionsResult.GetUsedIncludeFiles()) {
@@ -375,6 +376,8 @@ void ExporterHelper::SerializeRuntimeGameOptions(
gd::SerializerElement &runtimeGameOptions) {
// Create the setup options passed to the gdjs.RuntimeGame
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
runtimeGameOptions.AddChild("runtimeFilesBaseUrl")
.SetStringValue(GetExportedRuntimeFilesBaseUrl(fs, gdjsRoot));
auto &initialRuntimeGameStatus =
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
@@ -864,6 +867,20 @@ bool ExporterHelper::ExportCordovaFiles(const gd::Project &project,
.FindAndReplace("*", "");
};
auto makeOrientationPreference = [&project]() {
const gd::String &orientation = project.GetOrientation();
if (orientation != "landscape" && orientation != "portrait") {
return gd::String("");
}
// Declare the orientation at build time so that the native app only
// supports it. This is required on iPad: an app declaring support for all
// orientations is a "multitasking" app, for which iOS ignores any
// orientation lock requested at runtime.
return "<preference name=\"Orientation\" value=\"" + orientation +
"\" />";
};
gd::String str =
fs.ReadFile(gdjsRoot + "/Runtime/Cordova/config.xml")
.FindAndReplace("GDJS_PROJECTNAME",
@@ -874,7 +891,9 @@ bool ExporterHelper::ExportCordovaFiles(const gd::Project &project,
gd::Serializer::ToEscapedXMLString(project.GetPackageName()))
.FindAndReplace("GDJS_PROJECTVERSION", project.GetVersion())
.FindAndReplace("<!-- GDJS_ICONS_ANDROID -->", makeIconsAndroid())
.FindAndReplace("<!-- GDJS_ICONS_IOS -->", makeIconsIos());
.FindAndReplace("<!-- GDJS_ICONS_IOS -->", makeIconsIos())
.FindAndReplace("<!-- GDJS_ORIENTATION -->",
makeOrientationPreference());
gd::String plugins = "";
auto dependenciesAndExtensions =
@@ -1154,7 +1173,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool includeCaptureManager,
bool includeInAppTutorialMessage,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles) {
std::vector<gd::String> &includesFiles,
std::vector<gd::String> &resourcesFiles) {
// First, do not forget common includes (they must be included before events
// generated code files).
InsertUnique(includesFiles, "libs/jshashtable.js");
@@ -1244,8 +1264,12 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
if (pixiInThreeRenderers || isInGameEdition) {
InsertUnique(includesFiles, "pixi-renderers/three.js");
InsertUnique(includesFiles, "pixi-renderers/ThreeAddons.js");
InsertUnique(includesFiles, "pixi-renderers/draco/gltf/draco_decoder.wasm");
InsertUnique(includesFiles,
// The Draco decoder files are not included with a script tag: they are
// fetched by the DRACOLoader (of ThreeAddons.js) when a 3D model
// compressed with Draco must be read.
InsertUnique(resourcesFiles,
"pixi-renderers/draco/gltf/draco_decoder.wasm");
InsertUnique(resourcesFiles,
"pixi-renderers/draco/gltf/draco_wasm_wrapper.js");
// Extensions in JS may use it.
InsertUnique(includesFiles, "Extensions/3D/Scene3DTools.js");
@@ -1372,6 +1396,19 @@ bool ExporterHelper::ExportScenesEventsCode(
return true;
}
gd::String ExporterHelper::GetExportedRuntimeFilesBaseUrl(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot) {
// Same convention as in `GetExportedIncludeFilename`: the game engine files
// keep, relative to the "<GDJS Root>/Runtime" folder, the same relative
// path when exported. Some file systems use a URL for `gdjsRoot`, and keep
// absolute URLs pointing to the server the game engine files are served
// from.
gd::String runtimeFilesBaseUrl = gdjsRoot + "/Runtime/";
fs.MakeRelative(runtimeFilesBaseUrl, gdjsRoot + "/Runtime/");
return runtimeFilesBaseUrl.empty() ? "./" : runtimeFilesBaseUrl;
}
gd::String ExporterHelper::GetExportedIncludeFilename(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst) {
+18 -2
View File
@@ -544,7 +544,9 @@ class ExporterHelper {
gd::String exportDir);
/**
* \brief Add libraries files to the list of includes.
* \brief Add libraries files to the list of includes, and the files they
* need at runtime to the list of resources files (these are copied next to
* the game, but not included with a script tag).
*/
void AddLibsInclude(bool pixiRenderers,
bool pixiInThreeRenderers,
@@ -555,7 +557,8 @@ class ExporterHelper {
bool includeCaptureManager,
bool includeInAppTutorialMessage,
gd::String gdevelopLogoStyle,
std::vector<gd::String> &includesFiles);
std::vector<gd::String> &includesFiles,
std::vector<gd::String> &resourcesFiles);
/**
* \brief Remove include files that are Pixi renderers.
@@ -727,6 +730,19 @@ class ExporterHelper {
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst = 0);
/**
* \brief Returns the base URL (or relative path) from which the game engine
* files are served in the exported game or preview.
*
* Most of the time, they are copied next to the game index.html (in which
* case this is "./"), but some previews serve them from a server.
*
* \param fs The abstract file system to use
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
*/
static gd::String GetExportedRuntimeFilesBaseUrl(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot);
/**
* \brief Change the directory where code files are generated.
*
+6 -2
View File
@@ -46,8 +46,10 @@
<preference name="SwiftVersion" value="5.3" />
<!-- Required by admob-plus plugin > version 2 -->
<preference name="deployment-target" value="13.0" />
<!-- Apple requires a minimum OS version of 15.0 for App Store uploads
(ITMS-90068, enforced from Spring 2027) and Xcode 27 refuses to build
anything lower. -->
<preference name="deployment-target" value="15.0" />
<!-- GDJS_ICONS_IOS -->
</platform>
@@ -56,6 +58,8 @@
<preference name="BackgroundColor" value="0xff000000" />
<!-- GDJS_ORIENTATION -->
<!-- Android Fullscreen -->
<preference name="Fullscreen" value="true" />
+141 -15
View File
@@ -265,6 +265,48 @@ namespace gdjs {
// Keep the pickers intact for interaction - they're invisible anyway
}
/**
* Remove the near-infinite white guide lines shown by the Three.js TransformControls
* when an axis is hovered or dragged (helpers named X/Y/Z for translate/scale,
* AXIS for rotate). They cross the whole scene, which is distracting -
* especially when manipulating small objects.
*/
function patchAxisGuideLinesOnTransformControlsGizmos(
controls: THREE_ADDONS.TransformControls
) {
const gizmo = (controls as any)._gizmo;
if (!gizmo || !gizmo.helper) return;
// Only the axis guide lines are removed: the other helpers (START/END/DELTA,
// giving a discreet feedback of the movement while dragging) are kept.
const guideLineNamesByMode = {
translate: ['X', 'Y', 'Z'],
rotate: ['AXIS'],
scale: ['X', 'Y', 'Z'],
};
Object.keys(guideLineNamesByMode).forEach((mode) => {
const helperGroup = gizmo.helper[mode];
if (!helperGroup || !helperGroup.children) return;
const guideLines = helperGroup.children.filter((child) =>
guideLineNamesByMode[mode].includes(child.name)
);
guideLines.forEach((guideLine) => {
helperGroup.remove(guideLine);
// Geometries and materials are cloned per handle by TransformControls,
// so they can safely be disposed.
if (guideLine.geometry) guideLine.geometry.dispose();
if (Array.isArray(guideLine.material)) {
guideLine.material.forEach((material) => material.dispose());
} else if (guideLine.material) {
guideLine.material.dispose();
}
});
});
}
const getSvgIconUrl = (game: RuntimeGame, resourceName: string) => {
const resource = game.getResourceLoader().getResource(resourceName);
if (!resource) return '';
@@ -549,6 +591,22 @@ namespace gdjs {
const snap = (value: float, size: float, offset: float) =>
size ? offset + size * Math.round((value - offset) / size) : value;
/**
* Convert a scale ratio to the ratio giving a size whose far edge
* (origin + size) is on the grid. The size is kept at least 1.
*/
const getScaleSnappedOnGrid = (
origin: float,
initialSize: float,
scale: float,
snapPosition: (position: float) => float
): float => {
if (initialSize <= 0) return scale;
const snappedEdge = snapPosition(origin + initialSize * Math.abs(scale));
const snappedSize = Math.max(1, snappedEdge - origin);
return snappedSize / initialSize;
};
class Selection {
private _selectedObjects: Array<gdjs.RuntimeObject> = [];
@@ -2352,6 +2410,9 @@ namespace gdjs {
patchNegativeAxisHandlesOnTransformControlsGizmos(
threeTransformControls
);
patchAxisGuideLinesOnTransformControlsGizmos(
threeTransformControls
);
threeTransformControls.rotation.order = 'ZYX';
threeTransformControls.scale.y = -1;
@@ -2379,6 +2440,9 @@ namespace gdjs {
let initialObjectX = 0;
let initialObjectY = 0;
let initialObjectZ = 0;
let initialObjectWidth = 0;
let initialObjectHeight = 0;
let initialObjectDepth = 0;
const initialDummyPosition = new THREE.Vector3();
const initialDummyRotation = new THREE.Euler();
const initialDummyScale = new THREE.Vector3();
@@ -2398,6 +2462,11 @@ namespace gdjs {
initialObjectZ = is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getZ()
: 0;
initialObjectWidth = lastEditableSelectedObject.getWidth();
initialObjectHeight = lastEditableSelectedObject.getHeight();
initialObjectDepth = is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getDepth()
: 0;
initialDummyPosition.copy(dummyThreeObject.position);
initialDummyRotation.copy(dummyThreeObject.rotation);
initialDummyScale.copy(dummyThreeObject.scale);
@@ -2477,6 +2546,71 @@ namespace gdjs {
threeTransformControls.axis.length === 1
? 1
: 0.2;
let scaleX =
1 +
(dummyThreeObject.scale.x / initialDummyScale.x - 1) *
scaleDamping;
let scaleY =
1 +
(dummyThreeObject.scale.y / initialDummyScale.y - 1) *
scaleDamping;
let scaleZ =
1 +
(dummyThreeObject.scale.z / initialDummyScale.z - 1) *
scaleDamping;
if (
this._transformControlsMode === 'scale' &&
threeTransformControls.axis &&
this._editorGrid.isSpanningEnabled(inputManager)
) {
// The scale gizmo is anchored on the object origin, so snap the
// opposite edge on the grid (like the 2D editor resize handles).
const getSnappedScaleX = () =>
getScaleSnappedOnGrid(
initialObjectX,
initialObjectWidth,
scaleX,
(x) => this._editorGrid.getSnappedX(x)
);
const getSnappedScaleY = () =>
getScaleSnappedOnGrid(
initialObjectY,
initialObjectHeight,
scaleY,
(y) => this._editorGrid.getSnappedY(y)
);
const getSnappedScaleZ = () =>
getScaleSnappedOnGrid(
initialObjectZ,
initialObjectDepth,
scaleZ,
(z) => this._editorGrid.getSnappedZ(z)
);
if (threeTransformControls.axis === 'XYZ') {
// Uniform scaling: like the 2D proportional resize, snap the
// biggest side and apply the same ratio to the others.
const uniformScale =
initialObjectWidth >= initialObjectHeight &&
initialObjectWidth >= initialObjectDepth
? getSnappedScaleX()
: initialObjectHeight >= initialObjectDepth
? getSnappedScaleY()
: getSnappedScaleZ();
scaleX = uniformScale;
scaleY = uniformScale;
scaleZ = uniformScale;
} else {
if (threeTransformControls.axis.includes('X')) {
scaleX = getSnappedScaleX();
}
if (threeTransformControls.axis.includes('Y')) {
scaleY = getSnappedScaleY();
}
if (threeTransformControls.axis.includes('Z')) {
scaleZ = getSnappedScaleZ();
}
}
}
this._selectionControlsMovementTotalDelta = {
translationX,
translationY,
@@ -2490,18 +2624,9 @@ namespace gdjs {
rotationZ: -gdjs.toDegrees(
dummyThreeObject.rotation.z - initialDummyRotation.z
),
scaleX:
1 +
(dummyThreeObject.scale.x / initialDummyScale.x - 1) *
scaleDamping,
scaleY:
1 +
(dummyThreeObject.scale.y / initialDummyScale.y - 1) *
scaleDamping,
scaleZ:
1 +
(dummyThreeObject.scale.z / initialDummyScale.z - 1) *
scaleDamping,
scaleX,
scaleY,
scaleZ,
};
this._hasSelectionActuallyMoved =
@@ -2585,7 +2710,8 @@ namespace gdjs {
this._editorGrid.setTreeScene(threeScene);
}
this._editorGrid.setVisible(
this._transformControlsMode === 'translate'
this._transformControlsMode === 'translate' ||
this._transformControlsMode === 'scale'
);
}
}
@@ -4107,8 +4233,8 @@ namespace gdjs {
}
getSnappedZ(z: float): float {
const { gridDepth, gridOffsetY } = this;
return snap(z, gridDepth || 0, gridOffsetY);
const { gridDepth, gridOffsetZ } = this;
return snap(z, gridDepth || 0, gridOffsetZ);
}
isSpanningEnabled(
+12 -2
View File
@@ -38,7 +38,13 @@ namespace gdjs {
this._loader = new THREE_ADDONS.GLTFLoader();
this._dracoLoader = new THREE_ADDONS.DRACOLoader();
this._dracoLoader.setDecoderPath('./pixi-renderers/draco/gltf/');
// The Draco decoder files are shipped with the game engine files.
const runtimeFilesBaseUrl =
resourceLoader.getRuntimeGame().getAdditionalOptions()
.runtimeFilesBaseUrl || './';
this._dracoLoader.setDecoderPath(
runtimeFilesBaseUrl + 'pixi-renderers/draco/gltf/'
);
this._loader.setDRACOLoader(this._dracoLoader);
/**
@@ -153,7 +159,11 @@ namespace gdjs {
this._loadedThreeModels.clear();
this._downloadedArrayBuffers.clear();
this._loader = null;
this._dracoLoader = null;
if (this._dracoLoader) {
// Release the web workers used to decode the compressed 3D models.
this._dracoLoader.dispose();
this._dracoLoader = null;
}
if (this._invalidModel) {
this._invalidModel.cameras = [];
@@ -312,6 +312,12 @@ namespace gdjs {
* (`durationMs - loadingMs` close to it means the test is at risk of
* timing out on a slower machine). */
timeoutMs: number;
/** Time during which the game was frozen by the browser because the
* page was hidden (tab in the background, window minimized or covered
* by another one): animation frames stop, so the game stops stepping.
* Excluded from the `timeoutMs` budget - being looked away from is
* not a reason for a test to fail. */
hiddenStallMs: number;
gameTimeMs: number;
assertions: Array<GameplayTestAssertion>;
errors: Array<string>;
@@ -541,6 +547,17 @@ namespace gdjs {
/** Time spent waiting for loading (game boot, scene assets) so far:
* excluded from the `_timeoutMs` budget, reported as `loadingMs`. */
_loadingTimeMs: number = 0;
/** Time already spent frozen by the browser because the page was
* hidden, excluded from the `_timeoutMs` budget (see
* `_installPageVisibilityTracking`). */
_hiddenStallTimeMs: number = 0;
/** When the page became hidden, while it still is, or null. */
_hiddenSinceMs: number | null = null;
/** When the last game frame was stepped: tells apart a page that is
* hidden AND frozen from one that is only hidden (in the desktop app,
* background throttling is disabled and the game keeps running). */
_lastFrameStepTimeMs: number = 0;
_uninstallPageVisibilityTracking: () => void = () => {};
_maxFrames: integer;
/** Last time the stepping loop yielded to the browser (see
* `_maybeYield`). */
@@ -650,6 +667,60 @@ namespace gdjs {
}
}
/**
* Track the time during which the game is frozen because the page is
* hidden, so that it can be excluded from the `_timeoutMs` budget.
*
* Browsers stop `requestAnimationFrame` in a hidden page (a tab in
* the background, a minimized window, or - with Chrome's occlusion
* detection - a window entirely covered by another one). The test
* loop steps frames from animation frames, so the game simply stops:
* without this, a test would be reported as timing out just because
* the user looked at something else while it ran.
*
* Being hidden is not enough to conclude the game was frozen: the
* desktop app disables background throttling, and the game then keeps
* running while hidden. So only the time after the last stepped frame
* is counted as a stall.
*/
_installPageVisibilityTracking(): void {
if (typeof document === 'undefined') return;
const onVisibilityChanged = () => {
if (document.visibilityState === 'hidden') {
this._hiddenSinceMs = Date.now();
} else {
this._hiddenStallTimeMs += this._getOngoingHiddenStallMs();
this._hiddenSinceMs = null;
}
};
if (document.visibilityState === 'hidden') {
this._hiddenSinceMs = Date.now();
}
document.addEventListener('visibilitychange', onVisibilityChanged);
this._uninstallPageVisibilityTracking = () => {
document.removeEventListener('visibilitychange', onVisibilityChanged);
this._uninstallPageVisibilityTracking = () => {};
};
}
/** The stall of the hidden period currently in progress, if any. */
private _getOngoingHiddenStallMs(): number {
if (this._hiddenSinceMs === null) return 0;
// Frames stepped while hidden are frames the game was not frozen
// for: the stall only starts after the last of them.
const stallStartTimeMs = Math.max(
this._hiddenSinceMs,
this._lastFrameStepTimeMs
);
return Math.max(0, Date.now() - stallStartTimeMs);
}
/** Total time the game was frozen because the page was hidden. */
_getHiddenStallTimeMs(): number {
return this._hiddenStallTimeMs + this._getOngoingHiddenStallMs();
}
private _checkGuards(): void {
if (this._stopped) {
throw new GameplayTestStoppedError('The test was stopped.');
@@ -660,12 +731,15 @@ namespace gdjs {
);
}
if (
Date.now() - this._startTimeMs - this._loadingTimeMs >
Date.now() -
this._startTimeMs -
this._loadingTimeMs -
this._getHiddenStallTimeMs() >
this._timeoutMs
) {
throw new GameplayTestTimeoutError(
`The test timed out after ${this._timeoutMs}ms ` +
'(wall-clock, loading time excluded).'
'(wall-clock, loading and time spent hidden excluded).'
);
}
}
@@ -856,6 +930,7 @@ namespace gdjs {
this._framesExecuted++;
this._gameTimeMs += dtMs;
const stepTimeMs = Date.now() - stepStartTimeMs;
this._lastFrameStepTimeMs = stepStartTimeMs + stepTimeMs;
this._totalStepTimeMs += stepTimeMs;
if (stepTimeMs > this._worstStepTimeMs) {
this._worstStepTimeMs = stepTimeMs;
@@ -1348,6 +1423,48 @@ namespace gdjs {
return this._runtimeGame.getGameResolutionHeight();
}
/** Undo the game window size override (see `setGameResolutionSize`). */
_uninstallGameWindowSizeOverride: () => void = () => {};
/**
* Change the game resolution, as if the game window was resized to
* this size. Useful to test layouts made for other screen sizes,
* before asserting positions or taking a screenshot.
*
* This is the ONLY way the game resolution changes during a test:
* a gameplay test always runs in a game window of the project's game
* resolution, and moving, resizing or minimizing the gameplay test
* frame in the editor is never visible to the game (the frame is
* only a zoomed view of that fixed window).
*
* The engine sees a game window of this size until the end of the
* test: a game adapting its resolution to the game window size would
* otherwise recompute it from the real window, ignoring the size
* requested here.
*/
setGameResolutionSize(width: float, height: float): void {
this._uninstallGameWindowSizeOverride();
const original = {
getWindowInnerWidth: gdjs.RuntimeGameRenderer.getWindowInnerWidth,
getWindowInnerHeight: gdjs.RuntimeGameRenderer.getWindowInnerHeight,
};
gdjs.RuntimeGameRenderer.getWindowInnerWidth = () => width;
gdjs.RuntimeGameRenderer.getWindowInnerHeight = () => height;
this._uninstallGameWindowSizeOverride = () => {
gdjs.RuntimeGameRenderer.getWindowInnerWidth =
original.getWindowInnerWidth;
gdjs.RuntimeGameRenderer.getWindowInnerHeight =
original.getWindowInnerHeight;
this._uninstallGameWindowSizeOverride = () => {};
};
this._runtimeGame.setGameResolutionSize(width, height);
// The paused main loop would only adapt the cameras on its next
// animation frame: do it now, so the next stepped frame is laid out
// for the new resolution.
this._runtimeGame.getSceneStack().onGameResolutionResized();
}
/**
* Release all pressed keys, mouse buttons and touches.
*/
@@ -2854,6 +2971,7 @@ namespace gdjs {
durationMs: this._startTimeMs ? Date.now() - this._startTimeMs : 0,
loadingMs: Math.round(this._loadingTimeMs),
timeoutMs: this._timeoutMs,
hiddenStallMs: Math.round(this._getHiddenStallTimeMs()),
gameTimeMs: Math.round(this._gameTimeMs),
assertions: this._assertions,
errors: errors.slice(0, MAX_ERRORS),
@@ -3015,6 +3133,10 @@ namespace gdjs {
harness._installPointerLockShim();
harness._installSoundLog();
// Only from now on: the boot wait above is already excluded from the
// `timeoutMs` budget, as loading time.
harness._lastFrameStepTimeMs = Date.now();
harness._installPageVisibilityTracking();
// Capture the logs of the game itself (in addition to the `console`
// passed to the script).
@@ -3151,8 +3273,10 @@ namespace gdjs {
// Ignore errors during cleanup.
}
inputManager.onFrameEnded = originalOnFrameEnded;
harness._uninstallPageVisibilityTracking();
harness._uninstallPointerLockShim();
harness._uninstallSoundLog();
harness._uninstallGameWindowSizeOverride();
gdjs.Logger.setLoggerOutput(existingLoggerOutput);
if (payload.freezeWhenFinished) {
// Keep the game paused (the main loop keeps rendering the last
@@ -18,6 +18,28 @@ namespace gdjs {
return arr.indexOf(value) !== -1 ? value : min;
};
/**
* Wrap a Three.js shader so that its output colors are never negative.
* Negative colors become NaN in the sRGB conversion done by the output pass,
* which is rendered as garbage by some mobile GPUs (e.g. Mali).
*/
export const clampThreeShaderOutput = <
T extends { fragmentShader: string },
>(
shader: T
): T => ({
...shader,
fragmentShader:
shader.fragmentShader.replace(
/void\s+main\s*\(\s*\)/,
'void unclampedMain()'
) +
'\nvoid main() {\n' +
' unclampedMain();\n' +
' gl_FragColor.rgb = max(gl_FragColor.rgb, 0.0);\n' +
'}\n',
});
const _filterCreators: {
[filterName: string]: FilterCreator;
} = {};
+209 -1
View File
@@ -1,9 +1,33 @@
namespace gdjs {
const logger = new gdjs.Logger('Profiler');
/**
* @category Debugging > Profiler
*/
export type ProfilerStats = {
framesCount: integer;
/**
* The number of shader programs held by the 3D renderer at the end of the
* run (0 when the game does not render in 3D).
*/
shaderProgramsCount: integer;
/**
* The number of shader programs the 3D renderer had to compile *during*
* the run. Anything above 0 once the game is running means frames were
* spent compiling shaders instead of drawing - see
* `Profiler.recordShaderPrograms`.
*/
shaderProgramCompilationsCount: integer;
/** The number of captured frames that compiled at least one shader. */
framesWithShaderCompilationCount: integer;
/** Average number of 3D draw calls per captured frame. */
averageDrawCallsCount: float;
/** Average number of 3D triangles drawn per captured frame. */
averageTrianglesCount: float;
/** Geometries the 3D renderer was holding at the end of the run. */
geometriesCount: integer;
/** Textures the 3D renderer was holding at the end of the run. */
texturesCount: integer;
};
/**
@@ -56,6 +80,33 @@ namespace gdjs {
/** A function to get the current time. If available, corresponds to performance.now(). */
_getTimeNow: () => float;
/**
* The renderer cache keys already seen, or null until the first measured
* frame (whose programs are start-up cost, not part of the run).
*/
_seenShaderCacheKeys: Set<string> | null = null;
/** The number of shader programs held by the 3D renderer on the last frame. */
_shaderProgramsCount: integer = 0;
/** How many shader programs were compiled since profiling started. */
_shaderProgramCompilationsCount: integer = 0;
/** How many frames compiled at least one shader program. */
_framesWithShaderCompilationCount: integer = 0;
/** Draw calls summed over the frames measured, to average afterwards. */
_drawCallsSum: integer = 0;
/** Triangles summed over the frames measured, to average afterwards. */
_trianglesSum: integer = 0;
/** How many frames the two sums above cover. */
_rendererInfoFramesCount: integer = 0;
_geometriesCount: integer = 0;
_texturesCount: integer = 0;
constructor() {
while (this._framesMeasures.length < this._maxFramesCount) {
this._framesMeasures.push({
@@ -249,11 +300,168 @@ namespace gdjs {
return frameTimes;
}
/**
* Record what the 3D renderer did on this frame: its cheap per-frame
* counters, and the shader programs it is holding.
*
* The renderer compiles a separate program for every distinct combination
* of the things that shape a shader - material features, the number of
* lights of each kind, fog, shadow filtering, clipping planes, tone
* mapping, instancing, and more. Whenever the game moves any of them onto
* a combination that has not been seen before, that program is compiled
* and linked on the spot: the cost lands on a single frame, and never
* again for that combination. It shows up as unexplained stutter early in
* a playthrough, which is why it is worth measuring rather than guessing.
*
* Rather than assume a cause, this compares the cache keys the renderer
* itself uses to tell programs apart, and reports which fields differ from
* the closest program already seen.
*
* @param rendererInfo The current `WebGLRenderer.info`.
*/
record3DRendererInfo(rendererInfo: {
programs?: Array<{ cacheKey?: string; type?: string }> | null;
render?: { calls: number; triangles: number };
memory?: { geometries: number; textures: number };
}): void {
if (rendererInfo.render) {
// These counters are reset by the renderer on every frame, so reading
// them after rendering gives this frame's numbers.
this._drawCallsSum += rendererInfo.render.calls;
this._trianglesSum += rendererInfo.render.triangles;
this._rendererInfoFramesCount++;
}
if (rendererInfo.memory) {
this._geometriesCount = rendererInfo.memory.geometries;
this._texturesCount = rendererInfo.memory.textures;
}
const programs = rendererInfo.programs || null;
const programsCount = programs ? programs.length : 0;
this._shaderProgramsCount = programsCount;
if (!this._seenShaderCacheKeys) {
// First measured frame: whatever is already compiled is start-up
// cost, not something that happened during the run.
this._seenShaderCacheKeys = new Set<string>();
if (programs) {
for (const program of programs) {
this._seenShaderCacheKeys.add(program.cacheKey || '');
}
}
return;
}
if (!programs) {
return;
}
const newPrograms = programs.filter(
(program) => !this._seenShaderCacheKeys!.has(program.cacheKey || '')
);
if (!newPrograms.length) {
return;
}
const descriptions = newPrograms.map((program) => {
const description = Profiler._describeNewShaderProgram(
program,
this._seenShaderCacheKeys!
);
this._seenShaderCacheKeys!.add(program.cacheKey || '');
return description;
});
this._shaderProgramCompilationsCount += newPrograms.length;
this._framesWithShaderCompilationCount++;
logger.warn(
'Compiled ' +
newPrograms.length +
' new shader program(s) on frame ' +
this._framesCount +
' (' +
programsCount +
' in total), which costs a dropped frame. ' +
descriptions.join(' ')
);
}
/**
* Describe a newly compiled shader program by what makes it different from
* the most similar program already compiled.
*
* The renderer's cache keys are comma separated lists of the values that
* decide the shader, always in the same order, so comparing them field by
* field points straight at what the game changed - without this having to
* know what any given field means.
*/
private static _describeNewShaderProgram(
program: { cacheKey?: string; type?: string },
seenCacheKeys: Set<string>
): string {
const name = program.type || 'unknown material';
const cacheKey = program.cacheKey;
if (!cacheKey) {
return '"' + name + '" (no cache key to compare).';
}
const fields = cacheKey.split(',');
let closestDifferences: Array<string> | null = null;
for (const seenCacheKey of seenCacheKeys) {
const seenFields = seenCacheKey.split(',');
if (seenFields.length !== fields.length) {
// A different shader entirely, not the same one reconfigured.
continue;
}
const differences: Array<string> = [];
for (let i = 0; i < fields.length; i++) {
if (fields[i] !== seenFields[i]) {
differences.push(
'field ' + i + ': "' + seenFields[i] + '" -> "' + fields[i] + '"'
);
}
}
if (
differences.length &&
(!closestDifferences ||
differences.length < closestDifferences.length)
) {
closestDifferences = differences;
}
}
if (!closestDifferences) {
return '"' + name + '" is a shader that was not used before.';
}
return (
'"' +
name +
'" differs from the closest program already compiled in ' +
closestDifferences.length +
' field(s): ' +
closestDifferences.slice(0, 4).join(', ') +
(closestDifferences.length > 4 ? ', ...' : '') +
'.'
);
}
/**
* Get stats measured during the frames captured.
*/
getStats(): ProfilerStats {
return { framesCount: this._framesCount };
const rendererFrames = this._rendererInfoFramesCount || 1;
return {
framesCount: this._framesCount,
shaderProgramsCount: this._shaderProgramsCount,
shaderProgramCompilationsCount: this._shaderProgramCompilationsCount,
framesWithShaderCompilationCount:
this._framesWithShaderCompilationCount,
averageDrawCallsCount: this._drawCallsSum / rendererFrames,
averageTrianglesCount: this._trianglesSum / rendererFrames,
geometriesCount: this._geometriesCount,
texturesCount: this._texturesCount,
};
}
/**
+6
View File
@@ -108,6 +108,12 @@ namespace gdjs {
inGameEditorSettings?: InGameEditorSettings;
/**
* The base URL (or relative path) from which the game engine files are
* served. Most of the time, they are next to the game index.html ("./"),
* but some previews serve them from a server.
*/
runtimeFilesBaseUrl?: string;
/** Script files, used for hot-reloading. */
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without reloading libraries. */
+8
View File
@@ -446,6 +446,14 @@ namespace gdjs {
if (this._profiler) {
this._profiler.end('render');
}
if (this._profiler) {
const threeRenderer = this._runtimeGame
.getRenderer()
.getThreeRenderer();
if (threeRenderer) {
this._profiler.record3DRendererInfo(threeRenderer.info);
}
}
if (this._profiler) {
this._profiler.endFrame();
}
+46 -2
View File
@@ -2819,6 +2819,10 @@ interface WholeProjectRefactorer {
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_UpdateExtensionNameInExtension(
[Ref] Project project,
[Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString sourceExtensionName);
void STATIC_UpdateExtensionNameInEventsBasedBehavior(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
@@ -2834,6 +2838,13 @@ interface WholeProjectRefactorer {
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_MoveEventsFunction(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldExtensionName,
[Const] DOMString newExtensionName,
[Const] DOMString oldFunctionName,
[Const] DOMString newFunctionName);
void STATIC_RenameBehaviorEventsFunction(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
@@ -2912,6 +2923,13 @@ interface WholeProjectRefactorer {
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldName,
[Const] DOMString newName);
void STATIC_MoveEventsBasedBehavior(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldExtensionName,
[Const] DOMString newExtensionName,
[Const] DOMString oldBehaviorName,
[Const] DOMString newBehaviorName);
void STATIC_UpdateBehaviorNameInEventsBasedBehavior(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
@@ -2920,8 +2938,15 @@ interface WholeProjectRefactorer {
void STATIC_RenameEventsBasedObject(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldName,
[Const] DOMString newName);
[Const] DOMString oldObjectName,
[Const] DOMString newObjectName);
void STATIC_MoveEventsBasedObject(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
[Const] DOMString oldExtensionName,
[Const] DOMString newExtensionName,
[Const] DOMString oldObjectName,
[Const] DOMString newObjectName);
void STATIC_UpdateObjectNameInEventsBasedObject(
[Ref] Project project,
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
@@ -3103,6 +3128,10 @@ interface InstructionValidator {
[Const, Ref] Instruction instruction,
[Const, Ref] InstructionMetadata metadata,
long parameterIndex);
[Const, Value] DOMString STATIC_GetObjectNameForParameter(
[Const, Ref] ProjectScopedContainers projectScopedContainers,
[Const, Ref] Instruction instruction,
[Const] DOMString parameterType);
};
interface ObjectTools {
@@ -3130,6 +3159,20 @@ interface PropertyFunctionGenerator {
};
interface ChildObjectForwardFunctionGenerator {
void STATIC_GenerateChildObjectForwardFunctions(
[Const, Ref] Project project,
[Const, Ref] EventsFunctionsExtension extension,
[Ref] EventsBasedObject eventsBasedObject,
[Const] DOMString childObjectName);
boolean STATIC_HasAnyChildCustomObject(
[Const, Ref] Project project,
[Ref] EventsBasedObject eventsBasedObject);
[Value] VectorString STATIC_GetChildCustomObjectNames(
[Const, Ref] Project project,
[Ref] EventsBasedObject eventsBasedObject);
};
interface UsedExtensionsResult {
[Const, Ref] SetString GetUsedExtensions();
};
@@ -3279,6 +3322,7 @@ interface ExpressionValidator {
[Const, Ref] Platform platform,
[Const, Ref] ProjectScopedContainers projectScopedContainers,
[Const] DOMString rootType,
[Const] DOMString rootObjectName,
[Const] DOMString extraInfo);
[Const, Ref] VectorExpressionParserError GetAllErrors();
+11 -2
View File
@@ -53,7 +53,9 @@
#include <GDCore/IDE/Events/ExampleExtensionUsagesFinder.h>
#include <GDCore/IDE/EventsFunctionTools.h>
#include <GDCore/IDE/EventsBasedObjectVariantHelper.h>
#include <GDCore/IDE/EventsFunctionsExtensionExtractor.h>
#include <GDCore/IDE/ExtensionEditor/ChildObjectForwardFunctionGenerator.h>
#include <GDCore/IDE/ExtensionEditor/EventsFunctionsExtensionExtractor.h>
#include <GDCore/IDE/ExtensionEditor/PropertyFunctionGenerator.h>
#include <GDCore/IDE/ObjectRefactorer.h>
#include <GDCore/IDE/Project/ArbitraryResourceWorker.h>
#include <GDCore/IDE/Project/ArbitraryObjectsWorker.h>
@@ -65,7 +67,6 @@
#include <GDCore/IDE/Project/ResourcesRenamer.h>
#include <GDCore/IDE/Project/EventsBasedObjectDependencyFinder.h>
#include <GDCore/IDE/ProjectBrowserHelper.h>
#include <GDCore/IDE/PropertyFunctionGenerator.h>
#include <GDCore/IDE/UnfilledRequiredBehaviorPropertyProblem.h>
#include <GDCore/IDE/VariableInstructionSwitcher.h>
#include <GDCore/IDE/WholeProjectRefactorer.h>
@@ -745,6 +746,7 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_FillBehaviorParameters FillBehaviorParameters
#define STATIC_ValidateParameter ValidateParameter
#define STATIC_IsParameterValid IsParameterValid
#define STATIC_GetObjectNameForParameter GetObjectNameForParameter
#define STATIC_FixInvalidRequiredBehaviorProperties \
FixInvalidRequiredBehaviorProperties
#define STATIC_RemoveLayerInScene RemoveLayerInScene
@@ -762,6 +764,9 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_CanGenerateGetterAndSetter CanGenerateGetterAndSetter
#define STATIC_GenerateConditionSkeleton GenerateConditionSkeleton
#define STATIC_GenerateExpressionSkeleton GenerateExpressionSkeleton
#define STATIC_GenerateChildObjectForwardFunctions GenerateChildObjectForwardFunctions
#define STATIC_HasAnyChildCustomObject HasAnyChildCustomObject
#define STATIC_GetChildCustomObjectNames GetChildCustomObjectNames
#define STATIC_UpdateReturnActionType UpdateReturnActionType
#define STATIC_CreateRectangle CreateRectangle
#define STATIC_SanityCheckBehaviorProperty SanityCheckBehaviorProperty
@@ -820,11 +825,13 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_ComplyVariantsToEventsBasedObject ComplyVariantsToEventsBasedObject
#define STATIC_FindAllChildrenCustomObjectType FindAllChildrenCustomObjectType
#define STATIC_RenameEventsFunctionsExtension RenameEventsFunctionsExtension
#define STATIC_UpdateExtensionNameInExtension UpdateExtensionNameInExtension
#define STATIC_UpdateExtensionNameInEventsBasedBehavior \
UpdateExtensionNameInEventsBasedBehavior
#define STATIC_UpdateExtensionNameInEventsBasedObject \
UpdateExtensionNameInEventsBasedObject
#define STATIC_RenameEventsFunction RenameEventsFunction
#define STATIC_MoveEventsFunction MoveEventsFunction
#define STATIC_RenameBehaviorEventsFunction RenameBehaviorEventsFunction
#define STATIC_RenameObjectEventsFunction RenameObjectEventsFunction
#define STATIC_RenameParameter RenameParameter
@@ -842,8 +849,10 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
#define STATIC_ChangeEventsBasedBehaviorPropertyType ChangeEventsBasedBehaviorPropertyType
#define STATIC_ChangeEventsBasedObjectPropertyType ChangeEventsBasedObjectPropertyType
#define STATIC_RenameEventsBasedBehavior RenameEventsBasedBehavior
#define STATIC_MoveEventsBasedBehavior MoveEventsBasedBehavior
#define STATIC_UpdateBehaviorNameInEventsBasedBehavior UpdateBehaviorNameInEventsBasedBehavior
#define STATIC_RenameEventsBasedObject RenameEventsBasedObject
#define STATIC_MoveEventsBasedObject MoveEventsBasedObject
#define STATIC_UpdateObjectNameInEventsBasedObject UpdateObjectNameInEventsBasedObject
#define STATIC_RenameLayout RenameLayout
#define STATIC_RenameExternalLayout RenameExternalLayout
+82
View File
@@ -2483,6 +2483,87 @@ describe('libGD.js', function () {
});
});
describe('gd.ObjectsContainersList', function () {
it('lets a scene object group shadow a global object group with the same name', function () {
const project = gd.ProjectHelper.createNewGDJSProject();
const globalObject = project
.getObjects()
.insertNewObject(project, 'Sprite', 'GlobalObject', 0);
globalObject.addNewBehavior(
project,
'DraggableBehavior::Draggable',
'Draggable'
);
const globalGroup = project
.getObjects()
.getObjectGroups()
.insertNew('Group', 0);
globalGroup.addObject('GlobalObject');
const layout = project.insertNewLayout('Scene', 0);
// A scene group with the same name as the global group, empty for now.
const sceneGroup = layout
.getObjects()
.getObjectGroups()
.insertNew('Group', 0);
const objectsContainersList =
gd.ObjectsContainersList.makeNewObjectsContainersListForProjectAndLayout(
project,
layout
);
// The empty scene group is the only one considered: no type, no behaviors.
expect(objectsContainersList.getTypeOfObject('Group')).toBe('');
expect(
objectsContainersList.getBehaviorsOfObject('Group', true).size()
).toBe(0);
expect(
objectsContainersList.getTypeOfBehaviorInObjectOrGroup(
'Group',
'Draggable',
true
)
).toBe('');
// Once the scene group has an object, only this object is considered.
const sceneObject = layout
.getObjects()
.insertNewObject(project, 'TextObject::Text', 'SceneObject', 0);
sceneObject.addNewBehavior(
project,
'PlatformBehavior::PlatformerObjectBehavior',
'PlatformerObject'
);
sceneGroup.addObject('SceneObject');
expect(objectsContainersList.getTypeOfObject('Group')).toBe(
'TextObject::Text'
);
const behaviors = objectsContainersList
.getBehaviorsOfObject('Group', true)
.toJSArray();
expect(behaviors).toContain('PlatformerObject');
expect(behaviors).not.toContain('Draggable');
expect(
objectsContainersList.getTypeOfBehaviorInObjectOrGroup(
'Group',
'PlatformerObject',
true
)
).toBe('PlatformBehavior::PlatformerObjectBehavior');
expect(
objectsContainersList.getTypeOfBehaviorInObjectOrGroup(
'Group',
'Draggable',
true
)
).toBe('');
project.delete();
});
});
describe('gd.Instruction', function () {
it('initial values', function () {
let instr = new gd.Instruction();
@@ -3954,6 +4035,7 @@ describe('libGD.js', function () {
layout
),
type,
'',
''
);
expressionNode.visit(expressionValidator);
+39
View File
@@ -34,6 +34,7 @@ describe('libGD.js - GDJS related tests', function () {
<platform name="ios">
<!-- GDJS_ICONS_IOS -->
</platform>
<!-- GDJS_ORIENTATION -->
<!-- GDJS_EXTENSION_CORDOVA_DEPENDENCY -->
</widget>`;
const fakePackageJsonContent = `
@@ -133,6 +134,7 @@ describe('libGD.js - GDJS related tests', function () {
<platform name="ios">
</platform>
<preference name="Orientation" value="landscape" />
</widget>`
);
@@ -148,6 +150,42 @@ describe('libGD.js - GDJS related tests', function () {
}`
);
});
it('does not declare an orientation in Cordova config.xml for the default orientation', () => {
const project = gd.ProjectHelper.createNewGDJSProject();
project.setName('My game');
project.setVersion('1.2.3');
project.setOrientation('default');
var fs = makeFakeAbstractFileSystem(gd, {
'/fake-gdjs-root/Runtime/Cordova/www/index.html': fakeIndexHtmlContent,
'/fake-gdjs-root/Runtime/Cordova/config.xml': fakeConfigXmlContent,
'/fake-gdjs-root/Runtime/Cordova/package.json': fakePackageJsonContent,
'/fake-gdjs-root/Runtime/Cordova/www/LICENSE.GDevelop.txt': '',
});
const exporter = new gd.Exporter(fs, '/fake-gdjs-root');
const exportOptions = new gd.ExportOptions(project, '/fake-export-dir');
exportOptions.setTarget('cordova');
expect(exporter.exportWholePixiProject(exportOptions)).toBe(true);
exportOptions.delete();
exporter.delete();
expect(fs.writeToFile).toHaveBeenCalledWith(
'/fake-export-dir/config.xml',
`
<widget id="com.example.gamename" version="1.2.3">
<name>My game</name>
<platform name="android">
</platform>
<platform name="ios">
</platform>
</widget>`
);
});
it('properly exports Cordova plugins config if required by an extension', () => {
// Create a project with some content using the AdMob extension.
const project = gd.ProjectHelper.createNewGDJSProject();
@@ -191,6 +229,7 @@ describe('libGD.js - GDJS related tests', function () {
<platform name="ios">
</platform>
<preference name="Orientation" value="landscape" />
<plugin name=\"gdevelop-cordova-admob-plus\" spec=\"0.43.0\">
<variable name=\"APP_ID_ANDROID\" value=\"my android app id\" />
</plugin>
+13 -2
View File
@@ -2175,9 +2175,11 @@ export class WholeProjectRefactorer extends EmscriptenObject {
static applyRefactoringForObjectVariablesContainer(project: Project, objectVariablesContainer: VariablesContainer, initialInstancesContainer: InitialInstancesContainer, objectName: string, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
static applyRefactoringForGroupVariablesContainer(project: Project, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, initialInstancesContainer: InitialInstancesContainer, groupVariablesContainer: VariablesContainer, objectGroup: ObjectGroup, changeset: VariablesChangeset, originalSerializedVariables: SerializerElement): void;
static renameEventsFunctionsExtension(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
static updateExtensionNameInExtension(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, sourceExtensionName: string): void;
static updateExtensionNameInEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedBehavior: EventsBasedBehavior, sourceExtensionName: string): void;
static updateExtensionNameInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, sourceExtensionName: string): void;
static renameEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
static moveEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldFunctionName: string, newFunctionName: string): void;
static renameBehaviorEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedBehavior: EventsBasedBehavior, oldName: string, newName: string): void;
static renameObjectEventsFunction(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, oldName: string, newName: string): void;
static renameParameter(project: Project, projectScopedContainers: ProjectScopedContainers, eventsFunction: EventsFunction, parameterObjectsContainer: ObjectsContainer, oldName: string, newName: string): void;
@@ -2191,8 +2193,10 @@ export class WholeProjectRefactorer extends EmscriptenObject {
static renameEventsBasedObjectProperty(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, oldName: string, newName: string): void;
static changeEventsBasedObjectPropertyType(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, propertyName: string): void;
static renameEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
static moveEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldBehaviorName: string, newBehaviorName: string): void;
static updateBehaviorNameInEventsBasedBehavior(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedBehavior: EventsBasedBehavior, sourceBehaviorName: string): void;
static renameEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldName: string, newName: string): void;
static renameEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldObjectName: string, newObjectName: string): void;
static moveEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldObjectName: string, newObjectName: string): void;
static updateObjectNameInEventsBasedObject(project: Project, eventsFunctionsExtension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, sourceObjectName: string): void;
static renameLayout(project: Project, oldName: string, newName: string): void;
static renameExternalLayout(project: Project, oldName: string, newName: string): void;
@@ -2248,6 +2252,7 @@ export class ParameterValidationResult extends EmscriptenObject {
export class InstructionValidator extends EmscriptenObject {
static validateParameter(platform: Platform, projectScopedContainers: ProjectScopedContainers, instruction: Instruction, metadata: InstructionMetadata, parameterIndex: number): ParameterValidationResult;
static isParameterValid(platform: Platform, projectScopedContainers: ProjectScopedContainers, instruction: Instruction, metadata: InstructionMetadata, parameterIndex: number): boolean;
static getObjectNameForParameter(projectScopedContainers: ProjectScopedContainers, instruction: Instruction, parameterType: string): string;
}
export class ObjectTools extends EmscriptenObject {
@@ -2267,6 +2272,12 @@ export class PropertyFunctionGenerator extends EmscriptenObject {
static updateReturnActionType(project: Project, eventsFunction: EventsFunction): void;
}
export class ChildObjectForwardFunctionGenerator extends EmscriptenObject {
static generateChildObjectForwardFunctions(project: Project, extension: EventsFunctionsExtension, eventsBasedObject: EventsBasedObject, childObjectName: string): void;
static hasAnyChildCustomObject(project: Project, eventsBasedObject: EventsBasedObject): boolean;
static getChildCustomObjectNames(project: Project, eventsBasedObject: EventsBasedObject): VectorString;
}
export class UsedExtensionsResult extends EmscriptenObject {
getUsedExtensions(): SetString;
}
@@ -2380,7 +2391,7 @@ export class VectorExpressionParserError extends EmscriptenObject {
export class ExpressionParser2NodeWorker extends EmscriptenObject {}
export class ExpressionValidator extends EmscriptenObject {
constructor(platform: Platform, projectScopedContainers: ProjectScopedContainers, rootType: string, extraInfo: string);
constructor(platform: Platform, projectScopedContainers: ProjectScopedContainers, rootType: string, rootObjectName: string, extraInfo: string);
getAllErrors(): VectorExpressionParserError;
getFatalErrors(): VectorExpressionParserError;
getDeprecationWarnings(): VectorExpressionParserError;
@@ -0,0 +1,8 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdChildObjectForwardFunctionGenerator {
static generateChildObjectForwardFunctions(project: gdProject, extension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, childObjectName: string): void;
static hasAnyChildCustomObject(project: gdProject, eventsBasedObject: gdEventsBasedObject): boolean;
static getChildCustomObjectNames(project: gdProject, eventsBasedObject: gdEventsBasedObject): gdVectorString;
delete(): void;
ptr: number;
};
+1 -1
View File
@@ -1,6 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdExpressionValidator extends gdExpressionParser2NodeWorker {
constructor(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, rootType: string, extraInfo: string): void;
constructor(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, rootType: string, rootObjectName: string, extraInfo: string): void;
getAllErrors(): gdVectorExpressionParserError;
getFatalErrors(): gdVectorExpressionParserError;
getDeprecationWarnings(): gdVectorExpressionParserError;
@@ -2,6 +2,7 @@
declare class gdInstructionValidator {
static validateParameter(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, metadata: gdInstructionMetadata, parameterIndex: number): gdParameterValidationResult;
static isParameterValid(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, metadata: gdInstructionMetadata, parameterIndex: number): boolean;
static getObjectNameForParameter(projectScopedContainers: gdProjectScopedContainers, instruction: gdInstruction, parameterType: string): string;
delete(): void;
ptr: number;
};
@@ -5,9 +5,11 @@ declare class gdWholeProjectRefactorer {
static applyRefactoringForObjectVariablesContainer(project: gdProject, objectVariablesContainer: gdVariablesContainer, initialInstancesContainer: gdInitialInstancesContainer, objectName: string, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
static applyRefactoringForGroupVariablesContainer(project: gdProject, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, initialInstancesContainer: gdInitialInstancesContainer, groupVariablesContainer: gdVariablesContainer, objectGroup: gdObjectGroup, changeset: gdVariablesChangeset, originalSerializedVariables: gdSerializerElement): void;
static renameEventsFunctionsExtension(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
static updateExtensionNameInExtension(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, sourceExtensionName: string): void;
static updateExtensionNameInEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, sourceExtensionName: string): void;
static updateExtensionNameInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, sourceExtensionName: string): void;
static renameEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
static moveEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldFunctionName: string, newFunctionName: string): void;
static renameBehaviorEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, oldName: string, newName: string): void;
static renameObjectEventsFunction(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string): void;
static renameParameter(project: gdProject, projectScopedContainers: gdProjectScopedContainers, eventsFunction: gdEventsFunction, parameterObjectsContainer: gdObjectsContainer, oldName: string, newName: string): void;
@@ -21,8 +23,10 @@ declare class gdWholeProjectRefactorer {
static renameEventsBasedObjectProperty(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, oldName: string, newName: string): void;
static changeEventsBasedObjectPropertyType(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, propertyName: string): void;
static renameEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
static moveEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldBehaviorName: string, newBehaviorName: string): void;
static updateBehaviorNameInEventsBasedBehavior(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, sourceBehaviorName: string): void;
static renameEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, newName: string): void;
static renameEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldObjectName: string, newObjectName: string): void;
static moveEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, oldExtensionName: string, newExtensionName: string, oldObjectName: string, newObjectName: string): void;
static updateObjectNameInEventsBasedObject(project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, sourceObjectName: string): void;
static renameLayout(project: gdProject, oldName: string, newName: string): void;
static renameExternalLayout(project: gdProject, oldName: string, newName: string): void;
+1
View File
@@ -214,6 +214,7 @@ declare class libGDevelop {
ObjectTools: Class<gdObjectTools>;
EventsBasedObjectDependencyFinder: Class<gdEventsBasedObjectDependencyFinder>;
PropertyFunctionGenerator: Class<gdPropertyFunctionGenerator>;
ChildObjectForwardFunctionGenerator: Class<gdChildObjectForwardFunctionGenerator>;
UsedExtensionsResult: Class<gdUsedExtensionsResult>;
UsedExtensionsFinder: Class<gdUsedExtensionsFinder>;
UsedObjectTypeFinder: Class<gdUsedObjectTypeFinder>;
+2
View File
@@ -37,6 +37,8 @@ GDevelop is a powerful, no-code, open-source game engine. **GDevelop (BYOK Editi
### 🛡️ 4. Upstream Synchronized & Secure
- Continuously synchronized with upstream [4ian/GDevelop](https://github.com/4ian/GDevelop).
- Automated PR-only sync workflow with workflow locking to protect against supply-chain attacks (e.g. Shai-Hulud).
- Find GDevelop games on [gd.games](https://gd.games), the gaming platform for games powered by GDevelop.
- See the [showcase of games](https://gdevelop.io/games) created with GDevelop and published on Steam, iOS (App Store), Android (Google Play), Itch.io, Newgrounds, CrazyGames, Poki...
---
+4
View File
@@ -207,7 +207,11 @@ declare module "react-dnd" {
declare type DragDropMonitor = {
isDragging(): boolean,
didDrop(): boolean,
getItem(): any,
getItemType(): mixed,
getClientOffset(): ClientOffset | null,
subscribeToStateChange(listener: () => void): () => void,
...
};
+2 -2
View File
@@ -166,9 +166,9 @@
"start": "npm run import-resources && concurrently \"react-app-rewired start\" \"node scripts/watch-serve-GDJS-runtime.js\" -n \"editor,game engine\" --kill-others",
"start:vite": "npm run import-resources && vite",
"electron-app": "cd ../electron-app && npm run start",
"build": "npm run import-resources && react-app-rewired build",
"build": "npm run import-resources && react-app-rewired build && node scripts/check-build-output.js",
"build:vite": "npm run import-resources && vite build",
"build:dev": "npm run import-resources && REACT_APP_ENV=development react-app-rewired build && node scripts/copy-GDJS-Runtime-to-build.js",
"build:dev": "npm run import-resources && REACT_APP_ENV=development react-app-rewired build && node scripts/check-build-output.js && node scripts/copy-GDJS-Runtime-to-build.js",
"format": "prettier --write \"src/!(locales)/**/*.{js,ts,tsx}\"",
"check-format": "prettier --list-different \"src/!(locales)/**/*.{js,ts,tsx}\"",
"lint": "eslint src/ --max-warnings=0",
+50
View File
@@ -0,0 +1,50 @@
// @ts-check
const fs = require('fs');
const path = require('path');
const buildPath = path.join(__dirname, '..', 'build');
/**
* Verify that the build folder contains the files expected for a working
* app. This is important because the build can, in some cases, "succeed"
* while producing an empty or unusable build folder. For example, an error
* in a web worker file (compiled by `worker-loader` in a child compilation)
* is not reported as an error of the whole compilation - see
* `Resource3DPreview.worker.js`.
*/
const checkBuildOutput = () => {
const errors = [];
/** @param {string} directory @param {RegExp} pattern */
const checkFileExists = (directory, pattern) => {
const directoryPath = path.join(buildPath, directory);
const files = fs.existsSync(directoryPath)
? fs.readdirSync(directoryPath)
: [];
if (!files.some(file => pattern.test(file))) {
errors.push(
`Expected a file matching ${pattern} in "build/${directory}".`
);
}
};
checkFileExists('', /^index\.html$/);
checkFileExists('static/js', /^main\..*\.js$/);
checkFileExists('static/css', /^main\..*\.css$/);
// Workers are compiled separately by worker-loader: check they were
// properly built too.
checkFileExists('', /^Resource3DPreview\.worker\..*\.worker\.js$/);
checkFileExists('', /^BackgroundSerializer\.worker\..*\.worker\.js$/);
if (errors.length > 0) {
console.error(
'❌ The build folder is incomplete - the build silently failed:'
);
errors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
console.log('✅ Build folder content looks complete.');
};
checkBuildOutput();

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