Compare commits

...

114 Commits

Author SHA1 Message Date
Florian Rival
d41998ec1b Update posthog to latest version 2025-10-14 14:28:25 +02:00
Florian Rival
f917d1597e Fix leaderboard entry rate limiting (#7906)
- This enforce slimits of 12 successful saves per minute globally and 6 per minute per
leaderboard. This is enough for the very large majority of games and avoid issues when sending too many scores because of a mistake or wrong game design.
2025-10-14 12:28:09 +02:00
github-actions[bot]
d474c2a47e Update translations (#7899) 2025-10-13 13:24:23 +02:00
Florian Rival
086d4a1c6f Bump newIDE version 2025-10-13 13:17:18 +02:00
Florian Rival
65bc9ef4a8 Add missing ability to select text when a fatal error is displayed on screen 2025-10-12 17:15:49 +02:00
Florian Rival
073268160f Improve documentation pages generation [ci skip] 2025-10-12 14:23:50 +02:00
Florian Rival
40c576bc2d Fix Save State with old async tasks used in some extensions 2025-10-11 18:40:19 +02:00
Florian Rival
0bbf7ed804 Fix documentation formatting 2025-10-11 18:07:57 +02:00
Florian Rival
f05fa88eec Add collapsible blocks describing parameters of actions/conditions in the documentation 2025-10-11 17:49:19 +02:00
github-actions[bot]
6d67965c63 [Auto PR] Update translations (#7898) 2025-10-11 16:38:56 +02:00
Florian Rival
98a24dc8fc Add support for profiles and exclusions in Save States (#7897) 2025-10-11 15:47:31 +02:00
D8H
7fa55409ed Display both values and labels in selector properties (#7895) 2025-10-10 13:09:49 +02:00
D8H
20f2c0460e Automatically close the object editor dialog when editing the variant (#7894) 2025-10-10 11:52:04 +02:00
github-actions[bot]
dc36bf96f1 [Auto PR] Update translations (#7893) 2025-10-10 08:59:07 +02:00
Clément Pasteau
426e654f44 Open subscription dialog instead of credits dialog when reaching max daily AI requests (#7892) 2025-10-10 08:43:19 +02:00
Clément Pasteau
6d0f93c3ce Adapt editor function to duplicate object (#7889)
Do not show in changelog
2025-10-09 18:07:32 +02:00
Clément Pasteau
11d3c52197 Automatically scroll down the AI Panel when generating (#7888) 2025-10-08 15:17:11 +02:00
github-actions[bot]
2ee1050083 [Auto PR] Update translations (#7886) 2025-10-08 13:31:58 +02:00
Clément Pasteau
ca00afb918 Allow coming back from bundle purchase flow & improve display (#7887)
Do not show in changelog
2025-10-08 11:10:05 +02:00
Florian Rival
02953a1436 Fix warning 2025-10-08 09:34:04 +02:00
Florian Rival
e14215c679 Make GDJS runtime of the deployed web-app to use the exact commit hash
This avoids issues when deploying again the same version number

Don't show in changelog
2025-10-08 00:51:34 +02:00
github-actions[bot]
c60ea0701a [Auto PR] Update translations (#7885) 2025-10-07 12:43:15 +02:00
Digvijay Rawat
de6ae1cc8f Save the selected layer in each scene so it is persisted across sessions (#7879) 2025-10-07 12:31:47 +02:00
Clément Pasteau
ad24acd72f Allow buying a bundle without an account (#7873)
Do not show in changelog
2025-10-07 12:30:13 +02:00
Florian Rival
3c0bb83032 Add "Pick Nearest" and "Rotate toward object" action (#7883) 2025-10-07 11:59:07 +02:00
Florian Rival
d5c96d74ed Add a grace distance to Destroy Outside behavior to avoid deleting objects if they are never seen but near the camera (#7864) 2025-10-07 11:58:42 +02:00
github-actions[bot]
43aada8ae7 [Auto PR] Update translations (#7881) 2025-10-07 11:55:22 +02:00
Florian Rival
17a7c2815f Send GDevelop version to AI endpoints for backward compatibility
Don't show in changelog
2025-10-06 18:46:31 +02:00
Sebastien Dionne
9441774a22 Fix typos and linguistic errors in documentation / hacktoberfest (#7882)
Signed-off-by: Sebastien Dionne <survivant00@gmail.com>
2025-10-06 11:44:05 +02:00
Florian Rival
d279276cc0 Fix instance sometimes not visible after being added by the AI 2025-10-05 23:52:35 +02:00
Florian Rival
c7f8a7a2eb Fix formatting 2025-10-05 23:44:53 +02:00
D8H
0fb92e000b Fix the link to the asynchronous function help page (#7880) 2025-10-05 23:27:18 +02:00
Florian Rival
d70e3f71a6 Improve description of debugger tools extension [ci skip]
Don't show in changelog
2025-10-04 01:58:13 +02:00
Florian Rival
fe50ea3c01 Improve fog density description 2025-10-03 20:01:52 +02:00
danvervlad
77f821250a Add internal flag allowing to disable entirely hitboxes of Spine objects to avoid unnecessary recomputations (#7834)
Only show in developer changelog
2025-10-03 18:30:37 +02:00
github-actions[bot]
ff028ffa62 [Auto PR] Update translations (#7860)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-10-03 15:29:54 +02:00
D8H
64cc788d43 Save grid settings for variant tabs (#7869) 2025-10-02 14:15:41 +02:00
D8H
a85668c8d9 Fix aspect ratio being lost when hot-reloading 3D models (#7868) 2025-10-02 14:15:26 +02:00
Clément Pasteau
add042ec16 Create a standalone page that can be used for bundles (#7861) 2025-09-30 14:20:53 +02:00
Clément Pasteau
e174136fb4 New Save & Load built-in actions (#7811)
* New actions are available to save & load the game state, making Saving & Loading as easy as adding 1 action to your game!
* While it will work in most cases, it has a few limitations and hasn't been thoroughly tested on all types of objects/behaviors and games, so it is for the moment tagged as **Experimental** while we gather feedback and improve it
* Check out the wiki for more info: https://wiki.gdevelop.io/gdevelop5/all-features/save-state
2025-09-29 15:38:37 +02:00
github-actions[bot]
ff8697ed71 [Auto PR] Update translations (#7852) 2025-09-26 10:23:40 +02:00
ViktorVovk
542a841791 Add error handling for Howler sound methods (#7853)
* Specifically for "Maximum call stack size exceeded" errors.
2025-09-26 10:11:58 +02:00
github-actions[bot]
b9640f0049 [Auto PR] Update translations (#7851)
Do not show in changelog
2025-09-25 17:51:05 +02:00
Florian Rival
a6cc3dc85b Update storage action to make clearer the usage of manual load/unload 2025-09-25 16:28:36 +02:00
Clément Pasteau
6083c71e0e Bump newIDE version (#7850) 2025-09-25 10:48:53 +02:00
github-actions[bot]
e844ea819b Update extension translations [skip ci] (#7849)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-25 09:38:37 +02:00
github-actions[bot]
27f0300bc7 Update translations [skip ci] (#7845)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-25 09:35:00 +02:00
Clément Pasteau
35cea2eaba Update example slug info for analytics (#7847)
Do not show in changelog
2025-09-24 14:42:11 +02:00
D8H
7c4617da99 Add a skybox effect (#7843) 2025-09-23 21:14:44 +02:00
Clément Pasteau
d6d7c5c1fb Fix templates not accessible when bought via an unlisted bundle (#7844) 2025-09-23 11:18:15 +02:00
github-actions[bot]
b7f7a39aa7 Update translations [skip ci] (#7840)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-22 17:05:31 +02:00
Clément Pasteau
8c04771a87 Allow opening a bundle with a link via category (#7842)
Do not show in changelog
2025-09-22 16:36:16 +02:00
Clément Pasteau
febf15b279 Slightly improve Course page overload (#7841)
Do not show in changelog
2025-09-22 13:58:55 +02:00
D8H
a05e4b7ecc Fix groups wrongly underlined in red while having the right behaviors in the Events Sheet (#7839) 2025-09-22 11:25:37 +02:00
Florian Rival
3568a999f9 Update README [skip ci] [ci skip] 2025-09-18 16:48:48 +02:00
Florian Rival
207097bf03 Bump newIDE version 2025-09-18 10:01:44 +02:00
github-actions[bot]
0e3d2b9570 Update translations [skip ci] (#7831)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-09-18 10:01:32 +02:00
D8H
87fac429e8 Underline groups having missing behaviors in the Events Sheet (#7832) 2025-09-18 10:00:28 +02:00
D8H
3b1097931b Fix child object creation when called from another function (#7836) 2025-09-17 18:38:06 +02:00
D8H
ec7e408cd1 Fix button labels no longer updating in the editor (#7835) 2025-09-17 15:37:39 +02:00
Florian Rival
f8a99b9cfa Add prices and currency to store analytics (#7833)
Don't show in changelog
2025-09-16 15:44:35 +02:00
D8H
4c7231e6ae Fix a crash at runtime when behaviors are missing in functions (#7830) 2025-09-15 16:40:21 +02:00
D8H
883d32515c Rename "community" extensions as "experimental" extensions (#7828) 2025-09-15 14:04:31 +02:00
Florian Rival
f0f3c257fa Bump newIDE version 2025-09-14 16:11:02 +02:00
github-actions[bot]
42fce7d9ce Update translations [skip ci] (#7810)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-09-14 16:10:35 +02:00
Florian Rival
facac37fff Fix opening of the scene after creating project with AI
Don't show in changelog
2025-09-14 15:44:04 +02:00
Florian Rival
1272b601c6 Add many AI improvements (#7819)
* Allow the AI to start a game from a template
* Improved instances manipulation
* Better variables support
* Allow AI to swap an object by another one from the asset store
* Allow AI to change multiple properties at the same time
* Fix AI not properly removing behaviors associated to a behavior just removed
2025-09-13 18:01:35 +02:00
D8H
58e35cfaf5 Allow extensions to define labels for properties with string selectors (#7825) 2025-09-12 18:30:10 +02:00
D8H
2befc9781b Fix the list of assets suggested for sliders and toggle switches (#7826) 2025-09-12 15:42:10 +02:00
Florian Rival
8fb2872c36 Disable spell check on comments
Don't show in changelog
2025-09-12 10:25:41 +02:00
D8H
98033515c8 Fix a 1-frame delay when applying anchors to child-objects (#7820) 2025-09-11 15:19:45 +02:00
D8H
25c02cea2e Add a link to an help page for asynchronous functions (#7823) 2025-09-11 14:53:15 +02:00
D8H
6d5be78fec Forbid to import an extension which has the same name as a built-in one (#7822) 2025-09-11 14:52:49 +02:00
D8H
f6e60085db Fix missing extension dependencies in exported assets (#7821) 2025-09-11 14:52:32 +02:00
Florian Rival
a61648af70 Fix landscape/portrait orientation not working on iOS 2025-09-11 10:50:06 +02:00
D8H
2b496c6fd3 Fix custom object extraction behavior check (#7818)
- Don't show in changelog
2025-09-10 12:30:48 +02:00
Florian Rival
1e984f0965 Fix the chosen preset for AI not always valid when switching between chat and agent 2025-09-08 18:18:53 +02:00
Florian Rival
37bed36315 Make comment events editable exactly as they look (#7812) 2025-09-05 14:46:42 +02:00
D8H
5394cc5201 Forbid to add Physics behaviors on objects inside custom objects (#7809) 2025-09-04 19:37:57 +02:00
Florian Rival
5e3dfb0e9c Add condition to check if a key was just pressed (#7808) 2025-09-04 17:32:25 +02:00
Clément Pasteau
f6a6c981f8 Add a small intro explaining how to follow the courses (#7807) 2025-09-04 16:10:37 +02:00
Clément Pasteau
415c1bfd2f Fix bundles showing up in new object search (#7806) 2025-09-03 14:11:16 +02:00
github-actions[bot]
e4a911db25 Update translations [skip ci] (#7804)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-03 10:42:14 +02:00
Clément Pasteau
5fcd67d77b Additional bundles are added to the Learn & Shop sections (#7805)
* A premium bundle, including multiple courses, asset packs, templates and a gold subscription
* A curated platformer-specific bundle, including everything needed to learn & create a platformer game
2025-09-03 10:30:24 +02:00
Florian Rival
86db08ac3f Fix npm start on Windows
Don't show in changelog
2025-09-01 15:49:21 +02:00
github-actions[bot]
8d735fc726 Update translations [skip ci] (#7800)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-01 14:32:50 +02:00
Clément Pasteau
1e1f4bb2a3 Merge Bundle page into 1 component + small design fixes (#7803)
Do not show in changelog
2025-09-01 14:17:33 +02:00
Florian Rival
d8000aca10 Simplify editor README [skip ci] [ci skip] 2025-09-01 00:00:14 +02:00
Florian Rival
a2660ff0dc Allow to easily work on the game engine using the web-app development version (#7802) 2025-08-31 23:54:44 +02:00
Florian Rival
000d5785cf Improve AI agent performance by removing old object properties
Don't show in changelog
2025-08-30 17:29:49 +02:00
Florian Rival
9fe04712a9 Improve AI ability to update existing instances 2025-08-30 16:59:09 +02:00
Florian Rival
846afd9e0a Improve documentation with reference of all available effects. 2025-08-30 14:16:00 +02:00
Florian Rival
6125ff0f90 Allow AI to change layers/effects/scene and some game properties (#7799) 2025-08-29 19:15:50 +02:00
github-actions[bot]
a5428a8843 Update translations [skip ci] (#7798)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-08-29 15:58:38 +02:00
Ansel Games
19be45cda6 Fix grammar in text font size change warning (#7797) 2025-08-29 14:26:04 +02:00
Clément Pasteau
889c97cb27 Update Bundle Page with more details about content and limited offer (#7795) 2025-08-29 14:24:49 +02:00
Florian Rival
1d83da41a9 Improve physics extensions descriptions to tell about the scale and typical force values 2025-08-27 16:42:41 +02:00
github-actions[bot]
a65f2174eb Update translations [skip ci] (#7781)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-08-27 10:05:59 +02:00
D8H
9db493e87e Refactor tab opening (#7794)
- Don't show in changelogs
2025-08-26 17:29:42 +02:00
Florian Rival
49a3a18b51 Make clear in-app tutorials are free
Don't show in changelog
2025-08-26 15:53:55 +02:00
Florian Rival
0489e7036b Rework stories for TeamSection 2025-08-26 12:07:57 +02:00
Florian Rival
794d5a781c Add support for setting a full name for a student account (#7788) 2025-08-26 11:09:19 +02:00
D8H
c21dfbcc1f Optimize used resources search during export (#7790) 2025-08-25 20:12:17 +02:00
Clément Pasteau
cc75db6d09 Fix crash when accessing an owned archived product in the store (#7789)
Do not show in changelog
2025-08-25 17:03:30 +02:00
Florian Rival
48d35a50b5 Fix wrong redirection to AI tab when using initial-dialog=ask-ai in the web-app 2025-08-15 11:06:40 +02:00
Florian Rival
3a0888046f Add a selector to switch the AI used or choose a preset (#7782) 2025-08-14 16:14:39 +02:00
github-actions[bot]
7917994835 Update translations (#7763)
Co-authored-by: 4ian <1280130+4ian@users.noreply.github.com>
2025-08-14 13:55:44 +02:00
Florian Rival
9e2bab43f7 Rework display of course specializations
Don't show in changelog
2025-08-14 13:54:41 +02:00
Florian Rival
7e03f47f08 Bump newIDE version 2025-08-14 12:37:27 +02:00
Florian Rival
7c6137a4fc Ensure behaviors used in events generated by AI are automatically added to objects 2025-08-13 18:02:24 +02:00
D8H
0cbd6e2fe9 Fix cached materials not being cleared when unloading resources (#7780) 2025-08-11 15:50:22 +02:00
D8H
5acc1f5560 Remove unused imports (#7779)
Don't show in changelog
2025-08-11 12:30:49 +02:00
D8H
887693a90d Forbid camera zoom to be set to 0 (#7778) 2025-08-11 10:30:28 +02:00
D8H
fbb985833f Optimize JavaScript events (avoid a list copy) (#7775)
Only show in developer changelog
2025-08-09 17:58:12 +02:00
488 changed files with 40851 additions and 8065 deletions

View File

@@ -42,15 +42,19 @@ gd::String EventsCodeGenerator::GenerateRelationalOperatorCall(
const vector<gd::String>& arguments,
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t relationalOperatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
std::size_t relationalOperatorIndex =
instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "relationalOperator") {
if (instrInfos.parameters.GetParameter(i).GetType() ==
"relationalOperator") {
relationalOperatorIndex = i;
}
}
// Ensure that there is at least one parameter after the relational operator
if (relationalOperatorIndex + 1 >= instrInfos.parameters.GetParametersCount()) {
if (relationalOperatorIndex + 1 >=
instrInfos.parameters.GetParametersCount()) {
ReportError();
return "";
}
@@ -87,20 +91,23 @@ gd::String EventsCodeGenerator::GenerateRelationalOperation(
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) {
return lhs + " " + GenerateRelationalOperatorCodes(relationalOperator) + " " + rhs;
return lhs + " " + GenerateRelationalOperatorCodes(relationalOperator) + " " +
rhs;
}
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(const gd::String &operatorString) {
if (operatorString == "=") {
return "==";
}
if (operatorString != "<" && operatorString != ">" &&
operatorString != "<=" && operatorString != ">=" && operatorString != "!=" &&
operatorString != "startsWith" && operatorString != "endsWith" && operatorString != "contains") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
return "==";
}
return operatorString;
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(
const gd::String& operatorString) {
if (operatorString == "=") {
return "==";
}
if (operatorString != "<" && operatorString != ">" &&
operatorString != "<=" && operatorString != ">=" &&
operatorString != "!=" && operatorString != "startsWith" &&
operatorString != "endsWith" && operatorString != "contains") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
return "==";
}
return operatorString;
}
/**
@@ -124,7 +131,8 @@ gd::String EventsCodeGenerator::GenerateOperatorCall(
const gd::String& getterStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -195,7 +203,8 @@ gd::String EventsCodeGenerator::GenerateCompoundOperatorCall(
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -248,7 +257,8 @@ gd::String EventsCodeGenerator::GenerateMutatorCall(
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -323,83 +333,97 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
}
// Insert code only parameters and be sure there is no lack of parameter.
while (condition.GetParameters().size() < instrInfos.parameters.GetParametersCount()) {
while (condition.GetParameters().size() <
instrInfos.parameters.GetParametersCount()) {
vector<gd::Expression> parameters = condition.GetParameters();
parameters.push_back(gd::Expression(""));
condition.SetParameters(parameters);
}
gd::EventsCodeGenerator::CheckBehaviorParameters(condition, instrInfos);
// Verify that there are no mismatches between object type in parameters.
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
if (ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType())) {
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount();
++pNb) {
if (ParameterMetadata::IsObject(
instrInfos.parameters.GetParameter(pNb).GetType())) {
gd::String objectInParameter =
condition.GetParameter(pNb).GetPlainString();
const auto &expectedObjectType =
const auto& expectedObjectType =
instrInfos.parameters.GetParameter(pNb).GetExtraInfo();
const auto &actualObjectType =
const auto& actualObjectType =
GetObjectsContainersList().GetTypeOfObject(objectInParameter);
if (!GetObjectsContainersList().HasObjectOrGroupNamed(
objectInParameter)) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::UnknownObject, "",
objectInParameter, "");
gd::ProjectDiagnostic::ErrorType::UnknownObject,
"",
objectInParameter,
"");
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Unknown object - skipped. */";
} else if (!expectedObjectType.empty() &&
actualObjectType != expectedObjectType) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType, "",
actualObjectType, expectedObjectType, objectInParameter);
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType,
"",
actualObjectType,
expectedObjectType,
objectInParameter);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Mismatched object type - skipped. */";
}
}
}
bool isAnyBehaviorMissing =
gd::EventsCodeGenerator::CheckBehaviorParameters(condition, instrInfos);
if (isAnyBehaviorMissing) {
return "/* Missing behavior - skipped. */";
}
if (instrInfos.IsObjectInstruction()) {
gd::String objectName = condition.GetParameter(0).GetPlainString();
if (!objectName.empty() && instrInfos.parameters.GetParametersCount() > 0) {
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(objectName, context.GetCurrentObject());
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
for (std::size_t i = 0; i < realObjects.size(); ++i) {
// Set up the context
gd::String objectType = GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
gd::String objectType =
GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
const ObjectMetadata& objInfo =
MetadataProvider::GetObjectMetadata(platform, objectType);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
// Prepare arguments and generate the condition whole code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateObjectCondition(realObjects[i],
objInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
// Prepare arguments and generate the condition whole code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateObjectCondition(realObjects[i],
objInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
context.SetNoCurrentObject();
context.SetNoCurrentObject();
}
}
} else if (instrInfos.IsBehaviorInstruction()) {
if (instrInfos.parameters.GetParametersCount() >= 2) {
const gd::String &objectName = condition.GetParameter(0).GetPlainString();
const gd::String &behaviorName =
const gd::String& objectName = condition.GetParameter(0).GetPlainString();
const gd::String& behaviorName =
condition.GetParameter(1).GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehavior(behaviorName);
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
const BehaviorMetadata &autoInfo =
const BehaviorMetadata& autoInfo =
MetadataProvider::GetBehaviorMetadata(platform, actualBehaviorType);
for (std::size_t i = 0; i < realObjects.size(); ++i) {
@@ -411,15 +435,14 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
// Prepare arguments and generate the whole condition code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateBehaviorCondition(
realObjects[i],
behaviorName,
autoInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
conditionCode += GenerateBehaviorCondition(realObjects[i],
behaviorName,
autoInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
context.SetNoCurrentObject();
}
@@ -488,31 +511,50 @@ gd::String EventsCodeGenerator::GenerateConditionsListCode(
return outputCode;
}
void EventsCodeGenerator::CheckBehaviorParameters(
const gd::Instruction &instruction,
const gd::InstructionMetadata &instrInfos) {
gd::ParameterMetadataTools::IterateOverParameters(
instruction.GetParameters(), instrInfos.parameters,
[this](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue,
const gd::String &lastObjectName) {
bool EventsCodeGenerator::CheckBehaviorParameters(
const gd::Instruction& instruction,
const gd::InstructionMetadata& instrInfos) {
bool isAnyBehaviorMissing = false;
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
instrInfos.parameters,
[this, &isAnyBehaviorMissing, &instrInfos](
const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName,
size_t lastObjectIndex) {
if (ParameterMetadata::IsBehavior(parameterMetadata.GetType())) {
const gd::String &behaviorName = parameterValue.GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& behaviorName = parameterValue.GetPlainString();
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehaviorInObjectOrGroup(
lastObjectName, behaviorName);
const gd::String &expectedBehaviorType =
const gd::String& expectedBehaviorType =
parameterMetadata.GetExtraInfo();
if (!expectedBehaviorType.empty() &&
actualBehaviorType != expectedBehaviorType) {
const auto& objectParameterMetadata =
instrInfos.GetParameter(lastObjectIndex);
// Event functions crash if some objects in a group are missing
// the required behaviors, since they lose reference to the original
// objects. Missing behaviors are considered "fatal" only for
// ObjectList parameters, in order to minimize side effects on
// built-in functions.
if (objectParameterMetadata.GetType() == "objectList") {
isAnyBehaviorMissing = true;
}
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MissingBehavior, "",
actualBehaviorType, expectedBehaviorType, lastObjectName);
gd::ProjectDiagnostic::ErrorType::MissingBehavior,
"",
actualBehaviorType,
expectedBehaviorType,
lastObjectName);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
}
}
});
return isAnyBehaviorMissing;
}
/**
@@ -521,7 +563,8 @@ void EventsCodeGenerator::CheckBehaviorParameters(
gd::String EventsCodeGenerator::GenerateActionCode(
gd::Instruction& action,
EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
const gd::InstructionMetadata& instrInfos =
@@ -546,39 +589,51 @@ gd::String EventsCodeGenerator::GenerateActionCode(
: instrInfos.codeExtraInformation.functionCallName;
// Be sure there is no lack of parameter.
while (action.GetParameters().size() < instrInfos.parameters.GetParametersCount()) {
while (action.GetParameters().size() <
instrInfos.parameters.GetParametersCount()) {
vector<gd::Expression> parameters = action.GetParameters();
parameters.push_back(gd::Expression(""));
action.SetParameters(parameters);
}
gd::EventsCodeGenerator::CheckBehaviorParameters(action, instrInfos);
// Verify that there are no mismatches between object type in parameters.
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
if (ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType())) {
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount();
++pNb) {
if (ParameterMetadata::IsObject(
instrInfos.parameters.GetParameter(pNb).GetType())) {
gd::String objectInParameter = action.GetParameter(pNb).GetPlainString();
const auto &expectedObjectType =
const auto& expectedObjectType =
instrInfos.parameters.GetParameter(pNb).GetExtraInfo();
const auto &actualObjectType =
const auto& actualObjectType =
GetObjectsContainersList().GetTypeOfObject(objectInParameter);
if (!GetObjectsContainersList().HasObjectOrGroupNamed(
objectInParameter)) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::UnknownObject, "",
objectInParameter, "");
gd::ProjectDiagnostic::ErrorType::UnknownObject,
"",
objectInParameter,
"");
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Unknown object - skipped. */";
} else if (!expectedObjectType.empty() &&
actualObjectType != expectedObjectType) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType, "",
actualObjectType, expectedObjectType, objectInParameter);
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType,
"",
actualObjectType,
expectedObjectType,
objectInParameter);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Mismatched object type - skipped. */";
}
}
}
bool isAnyBehaviorMissing =
gd::EventsCodeGenerator::CheckBehaviorParameters(action, instrInfos);
if (isAnyBehaviorMissing) {
return "/* Missing behavior - skipped. */";
}
// Call free function first if available
if (instrInfos.IsObjectInstruction()) {
@@ -586,43 +641,46 @@ gd::String EventsCodeGenerator::GenerateActionCode(
if (instrInfos.parameters.GetParametersCount() > 0) {
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(objectName, context.GetCurrentObject());
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
for (std::size_t i = 0; i < realObjects.size(); ++i) {
// Setup context
gd::String objectType = GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
gd::String objectType =
GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
const ObjectMetadata& objInfo =
MetadataProvider::GetObjectMetadata(platform, objectType);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode += GenerateObjectAction(realObjects[i],
objInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode += GenerateObjectAction(realObjects[i],
objInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName,
optionalAsyncCallbackId);
context.SetNoCurrentObject();
context.SetNoCurrentObject();
}
}
} else if (instrInfos.IsBehaviorInstruction()) {
if (instrInfos.parameters.GetParametersCount() >= 2) {
const gd::String &objectName = action.GetParameter(0).GetPlainString();
const gd::String &behaviorName = action.GetParameter(1).GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& objectName = action.GetParameter(0).GetPlainString();
const gd::String& behaviorName = action.GetParameter(1).GetPlainString();
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehavior(behaviorName);
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
const BehaviorMetadata &autoInfo =
const BehaviorMetadata& autoInfo =
MetadataProvider::GetBehaviorMetadata(platform, actualBehaviorType);
AddIncludeFiles(autoInfo.includeFiles);
@@ -634,15 +692,15 @@ gd::String EventsCodeGenerator::GenerateActionCode(
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode +=
GenerateBehaviorAction(realObjects[i],
behaviorName,
autoInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
actionCode += GenerateBehaviorAction(realObjects[i],
behaviorName,
autoInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName,
optionalAsyncCallbackId);
context.SetNoCurrentObject();
}
@@ -654,7 +712,8 @@ gd::String EventsCodeGenerator::GenerateActionCode(
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
optionalAsyncCallbackName,
optionalAsyncCallbackId);
}
return actionCode;
@@ -667,8 +726,8 @@ gd::String EventsCodeGenerator::GenerateLocalVariablesStackAccessor() {
}
gd::String EventsCodeGenerator::GenerateAnyOrSceneVariableGetter(
const gd::Expression &variableExpression,
EventsCodeGenerationContext &context) {
const gd::Expression& variableExpression,
EventsCodeGenerationContext& context) {
const auto variableName = gd::ExpressionVariableNameFinder::GetVariableName(
*variableExpression.GetRootNode());
@@ -679,8 +738,12 @@ gd::String EventsCodeGenerator::GenerateAnyOrSceneVariableGetter(
: "scenevar";
return gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, variableParameterType,
variableExpression.GetPlainString(), "", "AllowUndeclaredVariable");
*this,
context,
variableParameterType,
variableExpression.GetPlainString(),
"",
"AllowUndeclaredVariable");
}
const EventsCodeGenerator::CallbackDescriptor
@@ -727,6 +790,11 @@ EventsCodeGenerator::GenerateCallback(
AddCustomCodeOutsideMain(callbackCode);
const gd::String idToCallbackMapUpdate = GetCodeNamespaceAccessor() +
"idToCallbackMap.set(" + callbackID +
", " + callbackFunctionName + ");\n";
AddCustomCodeOutsideMain(idToCallbackMapUpdate);
std::set<gd::String> requiredObjects;
// Build the list of all objects required by the callback. Any object that has
// already been declared could have gone through previous object picking, so
@@ -769,7 +837,7 @@ gd::String EventsCodeGenerator::GenerateActionsListCode(
} else {
outputCode += actionCode;
}
outputCode += "}";
outputCode += "}\n";
}
return outputCode;
@@ -786,13 +854,28 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
if (ParameterMetadata::IsExpression("number", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, "number", parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
"number",
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsExpression("string", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, "string", parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
"string",
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsExpression("variable", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, metadata.GetType(), parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
metadata.GetType(),
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsObject(metadata.GetType())) {
// It would be possible to run a gd::ExpressionCodeGenerator if later
// objects can have nested objects, or function returning objects.
@@ -827,7 +910,8 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
metadata.GetType() == "atlasResource" ||
metadata.GetType() == "spineResource" ||
// Deprecated, old parameter names:
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
metadata.GetType() == "password" ||
metadata.GetType() == "musicfile" ||
metadata.GetType() == "soundfile") {
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
} else if (metadata.GetType() == "mouse") {
@@ -977,7 +1061,8 @@ gd::String EventsCodeGenerator::GenerateEventsListCode(
for (std::size_t eId = 0; eId < events.size(); ++eId) {
auto& event = events[eId];
if (event.HasVariables()) {
GetProjectScopedContainers().GetVariablesContainersList().Push(event.GetVariables());
GetProjectScopedContainers().GetVariablesContainersList().Push(
event.GetVariables());
}
// Each event has its own context : Objects picked in an event are totally
@@ -1102,7 +1187,7 @@ gd::String EventsCodeGenerator::GenerateFreeCondition(
instrInfos.codeExtraInformation.functionCallName);
} else {
predicate = instrInfos.codeExtraInformation.functionCallName + "(" +
GenerateArgumentsList(arguments, 0) + ")";
GenerateArgumentsList(arguments, 0) + ")";
}
// Add logical not if needed
@@ -1146,7 +1231,7 @@ gd::String EventsCodeGenerator::GenerateObjectCondition(
instrInfos, arguments, objectFunctionCallNamePart, 1);
} else {
predicate = objectFunctionCallNamePart + "(" +
GenerateArgumentsList(arguments, 1) + ")";
GenerateArgumentsList(arguments, 1) + ")";
}
if (conditionInverted) predicate = GenerateNegatedPredicate(predicate);
@@ -1178,18 +1263,20 @@ gd::String EventsCodeGenerator::GenerateBehaviorCondition(
}
gd::String EventsCodeGenerator::GenerateFreeAction(
const gd::String& functionCallName,
const gd::String& functionCallName,
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Generate call
gd::String call;
if (instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
// Boolean actions declared with addExpressionAndConditionAndAction uses
// MutatorAndOrAccessor even though they don't declare an operator parameter.
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
// MutatorAndOrAccessor even though they don't declare an operator
// parameter. Boolean operators are only used with SetMutators or
// SetCustomCodeGenerator.
(instrInfos.codeExtraInformation.type == "boolean" &&
instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
@@ -1202,23 +1289,19 @@ gd::String EventsCodeGenerator::GenerateFreeAction(
instrInfos.codeExtraInformation.optionalAssociatedInstruction);
else if (instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::Mutators)
call =
GenerateMutatorCall(instrInfos,
arguments,
functionCallName);
call = GenerateMutatorCall(instrInfos, arguments, functionCallName);
else
call = GenerateCompoundOperatorCall(
instrInfos,
arguments,
functionCallName);
call =
GenerateCompoundOperatorCall(instrInfos, arguments, functionCallName);
} else {
call = functionCallName + "(" +
GenerateArgumentsList(arguments) + ")";
call = functionCallName + "(" + GenerateArgumentsList(arguments) + ")";
}
if (!optionalAsyncCallbackName.empty())
if (!optionalAsyncCallbackName.empty() && !optionalAsyncCallbackId.empty()) {
call = "runtimeScene.getAsyncTasksManager().addTask(" + call + ", " +
optionalAsyncCallbackName + ")";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList)";
}
return call + ";\n";
}
@@ -1230,7 +1313,8 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Create call
gd::String call;
if ((instrInfos.codeExtraInformation.type == "number" ||
@@ -1271,7 +1355,8 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Create call
gd::String call;
if ((instrInfos.codeExtraInformation.type == "number" ||
@@ -1286,17 +1371,13 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
2);
else
call = GenerateCompoundOperatorCall(
instrInfos,
arguments,
functionCallName,
2);
instrInfos, arguments, functionCallName, 2);
return "For each picked object \"" + objectName + "\", call " + call +
" for behavior \"" + behaviorName + "\".\n";
} else {
gd::String argumentsStr = GenerateArgumentsList(arguments, 2);
call = functionCallName + "(" +
argumentsStr + ")";
call = functionCallName + "(" + argumentsStr + ")";
return "For each picked object \"" + objectName + "\", call " + call + "(" +
argumentsStr + ")" + " for behavior \"" + behaviorName + "\"" +
@@ -1351,42 +1432,47 @@ gd::String EventsCodeGenerator::GenerateArgumentsList(
return argumentsStr;
}
gd::String EventsCodeGenerator::GeneratePropertyGetter(const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
gd::String EventsCodeGenerator::GeneratePropertyGetter(
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
return "getProperty" + property.GetName() + "As" + type + "()";
}
gd::String EventsCodeGenerator::GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) {
return "getProperty" + property.GetName() + "()";
}
gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) {
return "setProperty" + property.GetName() + "(" + operandCode + ")";
}
}
gd::String EventsCodeGenerator::GenerateParameterGetter(const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
gd::String EventsCodeGenerator::GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
return "getParameter" + parameter.GetName() + "As" + type + "()";
}
gd::String EventsCodeGenerator::GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) {
return "getParameter" + parameter.GetName() + "()";
}
const gd::ParameterMetadata& parameter) {
return "getParameter" + parameter.GetName() + "()";
}
EventsCodeGenerator::EventsCodeGenerator(const gd::Project& project_,
const gd::Layout& layout,
const gd::Platform& platform_)
: platform(platform_),
projectScopedContainers(gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project_, layout)),
projectScopedContainers(
gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project_,
layout)),
hasProjectAndLayout(true),
project(&project_),
scene(&layout),
@@ -1395,7 +1481,7 @@ EventsCodeGenerator::EventsCodeGenerator(const gd::Project& project_,
maxCustomConditionsDepth(0),
maxConditionsListsSize(0),
eventsListNextUniqueId(0),
diagnosticReport(nullptr){};
diagnosticReport(nullptr) {};
EventsCodeGenerator::EventsCodeGenerator(
const gd::Platform& platform_,
@@ -1410,6 +1496,6 @@ EventsCodeGenerator::EventsCodeGenerator(
maxCustomConditionsDepth(0),
maxConditionsListsSize(0),
eventsListNextUniqueId(0),
diagnosticReport(nullptr){};
diagnosticReport(nullptr) {};
} // namespace gd

View File

@@ -9,9 +9,9 @@
#include <utility>
#include <vector>
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
#include "GDCore/Events/Event.h"
#include "GDCore/Events/Instruction.h"
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
#include "GDCore/Project/ProjectScopedContainers.h"
#include "GDCore/String.h"
@@ -62,7 +62,7 @@ class GD_CORE_API EventsCodeGenerator {
EventsCodeGenerator(
const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers_);
virtual ~EventsCodeGenerator(){};
virtual ~EventsCodeGenerator() {};
/**
* \brief Preprocess an events list (replacing for example links with the
@@ -160,7 +160,8 @@ class GD_CORE_API EventsCodeGenerator {
gd::String GenerateActionCode(
gd::Instruction& action,
EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
struct CallbackDescriptor {
CallbackDescriptor(const gd::String functionName_,
@@ -168,7 +169,7 @@ class GD_CORE_API EventsCodeGenerator {
const std::set<gd::String> requiredObjects_)
: functionName(functionName_),
argumentsList(argumentsList_),
requiredObjects(requiredObjects_){};
requiredObjects(requiredObjects_) {};
/**
* The name by which the function can be invoked.
*/
@@ -338,9 +339,9 @@ class GD_CORE_API EventsCodeGenerator {
}
/**
* @brief Give access to the project scoped containers as code generation might
* push and pop variable containers (for local variables).
* This could be passed as a parameter recursively in code generation, but this requires
* @brief Give access to the project scoped containers as code generation
* might push and pop variable containers (for local variables). This could be
* passed as a parameter recursively in code generation, but this requires
* heavy refactoring. Instead, we use this single instance.
*/
gd::ProjectScopedContainers& GetProjectScopedContainers() {
@@ -387,9 +388,7 @@ class GD_CORE_API EventsCodeGenerator {
diagnosticReport = diagnosticReport_;
}
gd::DiagnosticReport* GetDiagnosticReport() {
return diagnosticReport;
}
gd::DiagnosticReport* GetDiagnosticReport() { return diagnosticReport; }
/**
* \brief Generate the full name for accessing to a boolean variable used for
@@ -513,16 +512,16 @@ class GD_CORE_API EventsCodeGenerator {
* \brief Generate an any variable getter that fallbacks on scene variable for
* compatibility reason.
*/
gd::String
GenerateAnyOrSceneVariableGetter(const gd::Expression &variableExpression,
EventsCodeGenerationContext &context);
gd::String GenerateAnyOrSceneVariableGetter(
const gd::Expression& variableExpression,
EventsCodeGenerationContext& context);
virtual gd::String GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode);
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode);
protected:
protected:
virtual const gd::String GenerateRelationalOperatorCodes(
const gd::String& operatorString);
@@ -643,16 +642,16 @@ protected:
gd::EventsCodeGenerationContext& context);
virtual gd::String GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property);
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property);
virtual gd::String GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context);
virtual gd::String
GenerateParameterGetterWithoutCasting(const gd::ParameterMetadata &parameter);
virtual gd::String GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata& parameter);
/**
* \brief Generate the code to reference an object which is
@@ -769,7 +768,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
virtual gd::String GenerateObjectAction(
const gd::String& objectName,
@@ -778,7 +778,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
virtual gd::String GenerateBehaviorAction(
const gd::String& objectName,
@@ -788,7 +789,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
gd::String GenerateRelationalOperatorCall(
const gd::InstructionMetadata& instrInfos,
@@ -837,9 +839,8 @@ protected:
virtual gd::String GenerateGetBehaviorNameCode(
const gd::String& behaviorName);
void CheckBehaviorParameters(
const gd::Instruction &instruction,
const gd::InstructionMetadata &instrInfos);
bool CheckBehaviorParameters(const gd::Instruction& instruction,
const gd::InstructionMetadata& instrInfos);
const gd::Platform& platform; ///< The platform being used.
@@ -876,4 +877,3 @@ protected:
};
} // namespace gd

View File

@@ -293,6 +293,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddCodeOnlyParameter("currentScene", "")
.MarkAsAdvanced();
obj.AddAction(
"RotateTowardObject",
_("Rotate toward another object"),
_("Rotate an object towards another object, with the specified speed. "
"Note that if multiple instances of the target object are picked, "
"only the first one will be used. Use a For Each event or actions "
"like \"Pick nearest object\", \"Pick a random object\" to refine "
"the choice of the target object."),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_ deg/second"),
_("Angle"),
"res/actions/rotate24_black.png",
"res/actions/rotate_black.png")
.AddParameter("object", _("Object"))
.AddParameter("objectPtr", _("Target object"))
.AddParameter("expression", _("Angular speed (in degrees per second)"))
.SetParameterLongDescription(_("Enter 0 for an immediate rotation."))
.AddCodeOnlyParameter("currentScene", "")
.MarkAsAdvanced();
obj.AddAction(
"AddForceXY",
_("Add a force"),
@@ -1617,7 +1636,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
extension
.AddAction("AjoutObjConcern",
_("Pick all instances"),
_("Pick all object instances"),
_("Pick all instances of the specified object(s). When you "
"pick all instances, "
"the next conditions and actions of this event work on all "
@@ -1631,20 +1650,34 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.MarkAsAdvanced();
extension
.AddAction(
"AjoutHasard",
_("Pick a random object"),
_("Pick one object from all the specified objects. When an object "
"is picked, the next conditions and actions of this event work "
"only on that object."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/actions/ajouthasard24.png",
"res/actions/ajouthasard.png")
.AddAction("AjoutHasard",
_("Pick a random object"),
_("Pick one instance from all the specified objects. When an "
"instance is picked, the next conditions and actions of "
"this event work only on that object instance."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/actions/ajouthasard24.png",
"res/actions/ajouthasard.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectList", _("Object"))
.MarkAsSimple();
extension
.AddAction(
"PickNearest",
_("Pick nearest object"),
_("Pick the instance of this object that is nearest to the specified "
"position."),
_("Pick the _PARAM0_ that is nearest to _PARAM1_;_PARAM2_"),
_("Objects"),
"res/conditions/distance24.png",
"res/conditions/distance.png")
.AddParameter("objectList", _("Object"))
.AddParameter("expression", _("X position"))
.AddParameter("expression", _("Y position"))
.MarkAsSimple();
extension
.AddAction(
"MoveObjects",
@@ -1694,11 +1727,12 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
extension
.AddCondition(
"AjoutObjConcern",
_("Pick all objects"),
_("Pick all the specified objects. When you pick all objects, "
_("Pick all object instances"),
_("Pick all instances of the specified object(s). When you "
"pick all instances, "
"the next conditions and actions of this event work on all "
"of them."),
_("Pick all _PARAM1_ objects"),
_("Pick all instances of _PARAM1_"),
_("Objects"),
"res/conditions/add24.png",
"res/conditions/add.png")
@@ -1707,16 +1741,15 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.MarkAsAdvanced();
extension
.AddCondition(
"AjoutHasard",
_("Pick a random object"),
_("Pick one object from all the specified objects. When an object "
"is picked, the next conditions and actions of this event work "
"only on that object."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/conditions/ajouthasard24.png",
"res/conditions/ajouthasard.png")
.AddCondition("AjoutHasard",
_("Pick a random object"),
_("Pick one instance from all the specified objects. When "
"an instance is picked, the next conditions and actions "
"of this event work only on that object instance."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/conditions/ajouthasard24.png",
"res/conditions/ajouthasard.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectList", _("Object"))
.MarkAsSimple();
@@ -1725,9 +1758,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddCondition(
"PickNearest",
_("Pick nearest object"),
_("Pick the object of this type that is nearest to the specified "
"position. If the condition is inverted, the object farthest from "
"the specified position is picked instead."),
_("Pick the instance of this object that is nearest to the specified "
"position. If the condition is inverted, the instance farthest "
"from the specified position is picked instead."),
_("Pick the _PARAM0_ that is nearest to _PARAM1_;_PARAM2_"),
_("Objects"),
"res/conditions/distance24.png",

View File

@@ -42,13 +42,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFileExtension(
extension
.AddAction(
"LoadFile",
_("Load a storage in memory"),
_("This action loads the specified storage in memory, so you can "
"write and read it.\nYou can open and write without using this "
"action, but it will be slower.\nIf you use this action, do not "
"forget to unload the storage from memory."),
_("Load storage _PARAM0_ in memory"),
"",
_("Manually preload a storage in memory"),
_("Forces the specified storage to be loaded and kept in "
"memory, allowing faster reads/writes. "
"However, it requires manual management: if you use this "
"action, you *must* also unload the storage manually when "
"it's no longer needed to ensure data is persisted.\n\n"
"Unless you have a specific performance need, avoid using this "
"action. The system already handles loading/unloading "
"automatically."),
_("Load data storage _PARAM0_ in memory"),
_("Advanced"),
"res/actions/fichier24.png",
"res/actions/fichier.png")
.AddParameter("string", _("Storage name"))
@@ -56,11 +60,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFileExtension(
extension
.AddAction("UnloadFile",
_("Close a storage"),
_("This action closes the structured data previously loaded "
_("Manually unload and persist a storage"),
_("Close the specified storage previously loaded "
"in memory, saving all changes made."),
_("Close structured data _PARAM0_"),
"",
_("Unload and persist data storage _PARAM0_"),
_("Advanced"),
"res/actions/fichier24.png",
"res/actions/fichier.png")
.AddParameter("string", _("Storage name"))

View File

@@ -52,10 +52,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
.SetHidden();
extension
.AddCondition("KeyFromTextPressed",
_("Key pressed"),
_("Check if a key is pressed"),
_("_PARAM1_ key is pressed"),
.AddCondition(
"KeyFromTextPressed",
_("Key pressed"),
_("Check if a key is pressed. This stays true as long as "
"the key is held down. To check if a key was pressed during "
"the frame, use \"Key just pressed\" instead."),
_("_PARAM1_ key is pressed"),
"",
"res/conditions/keyboard24.png",
"res/conditions/keyboard.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("keyboardKey", _("Key to check"))
.MarkAsSimple();
extension
.AddCondition("KeyFromTextJustPressed",
_("Key just pressed"),
_("Check if a key was just pressed."),
_("_PARAM1_ key was just pressed"),
"",
"res/conditions/keyboard24.png",
"res/conditions/keyboard.png")
@@ -66,7 +81,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
extension
.AddCondition("KeyFromTextReleased",
_("Key released"),
_("Check if a key was just released"),
_("Check if a key was just released."),
_("_PARAM1_ key is released"),
"",
"res/conditions/keyboard24.png",

View File

@@ -298,6 +298,19 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
return *this;
}
/**
* Check if the behavior can be used on objects from event-based objects.
*/
bool IsRelevantForChildObjects() const { return isRelevantForChildObjects; }
/**
* Set that behavior can't be used on objects from event-based objects.
*/
BehaviorMetadata &MarkAsIrrelevantForChildObjects() {
isRelevantForChildObjects = false;
return *this;
}
QuickCustomization::Visibility GetQuickCustomizationVisibility() const {
return quickCustomizationVisibility;
}
@@ -393,6 +406,7 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
mutable std::vector<gd::String> requiredBehaviors;
bool isPrivate = false;
bool isHidden = false;
bool isRelevantForChildObjects = true;
gd::String openFullEditorLabel;
QuickCustomization::Visibility quickCustomizationVisibility = QuickCustomization::Visibility::Default;

View File

@@ -277,6 +277,10 @@ class GD_CORE_API MetadataProvider {
return &metadata == &badObjectInfo;
}
static bool IsBadEffectMetadata(const gd::EffectMetadata& metadata) {
return &metadata == &badEffectMetadata;
}
virtual ~MetadataProvider();
private:

View File

@@ -194,7 +194,8 @@ void ParameterMetadataTools::IterateOverParameters(
[&fn](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
const gd::String& lastObjectName,
size_t lastObjectIndex) {
fn(parameterMetadata, parameterValue, lastObjectName);
});
}
@@ -205,8 +206,10 @@ void ParameterMetadataTools::IterateOverParametersWithIndex(
std::function<void(const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName)> fn) {
const gd::String& lastObjectName,
size_t lastObjectIndex)> fn) {
gd::String lastObjectName = "";
size_t lastObjectIndex = 0;
for (std::size_t pNb = 0; pNb < parametersMetadata.GetParametersCount();
++pNb) {
const gd::ParameterMetadata &parameterMetadata =
@@ -218,15 +221,17 @@ void ParameterMetadataTools::IterateOverParametersWithIndex(
? Expression(parameterMetadata.GetDefaultValue())
: parameterValue;
fn(parameterMetadata, parameterValueOrDefault, pNb, lastObjectName);
fn(parameterMetadata, parameterValueOrDefault, pNb, lastObjectName, lastObjectIndex);
// Memorize the last object name. By convention, parameters that require
// an object (mainly, "objectvar" and "behavior") should be placed after
// the object in the list of parameters (if possible, just after).
// Search "lastObjectName" in the codebase for other place where this
// convention is enforced.
if (gd::ParameterMetadata::IsObject(parameterMetadata.GetType()))
if (gd::ParameterMetadata::IsObject(parameterMetadata.GetType())) {
lastObjectName = parameterValueOrDefault.GetPlainString();
lastObjectIndex = pNb;
}
}
}

View File

@@ -64,7 +64,8 @@ class GD_CORE_API ParameterMetadataTools {
std::function<void(const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName)> fn);
const gd::String& lastObjectName,
size_t lastObjectIndex)> fn);
/**
* Iterate over the parameters of a FunctionCallNode.

View File

@@ -29,7 +29,7 @@ bool BehaviorParametersFiller::DoVisitInstruction(gd::Instruction &instruction,
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName) {
const gd::String &lastObjectName, size_t lastObjectIndex) {
if (parameterMetadata.GetValueTypeMetadata().IsBehavior() &&
parameterValue.GetPlainString().length() == 0) {

View File

@@ -108,12 +108,10 @@ bool EventsBehaviorRenamer::DoVisitInstruction(gd::Instruction& instruction,
platform, instruction.GetType());
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[&](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
const gd::String& type = parameterMetadata.GetType();
if (gd::ParameterMetadata::IsBehavior(type)) {

View File

@@ -183,12 +183,10 @@ bool EventsParameterReplacer::DoVisitInstruction(gd::Instruction& instruction,
platform, instruction.GetType());
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[&](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
if (!gd::EventsParameterReplacer::CanContainParameter(
parameterMetadata.GetValueTypeMetadata())) {
return;

View File

@@ -217,12 +217,10 @@ bool EventsPropertyReplacer::DoVisitInstruction(gd::Instruction& instruction,
bool shouldDeleteInstruction = false;
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[&](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
if (!gd::EventsPropertyReplacer::CanContainProperty(
parameterMetadata.GetValueTypeMetadata())) {
return;

View File

@@ -334,7 +334,7 @@ private:
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName) {
const gd::String &lastObjectName, size_t lastObjectIndex) {
if (!gd::EventsObjectReplacer::CanContainObject(
parameterMetadata.GetValueTypeMetadata())) {
return;

View File

@@ -42,18 +42,16 @@ bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction&
platform, instruction.GetType());
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[&](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
const gd::String& type = parameterMetadata.GetType();
if (!gd::ParameterMetadata::IsExpression("variable", type) ||
!gd::VariableInstructionSwitcher::IsSwitchableVariableInstruction(
instruction.GetType())) {
return;
return;
}
const auto variableName =
gd::ExpressionVariableNameFinder::GetVariableName(
@@ -72,10 +70,11 @@ bool EventsVariableInstructionTypeSwitcher::DoVisitInstruction(gd::Instruction&
.GetObjectOrGroupVariablesContainer(lastObjectName);
}
} else if (type == "variableOrProperty") {
variablesContainer =
&GetProjectScopedContainers()
.GetVariablesContainersList()
.GetVariablesContainerFromVariableOrPropertyName(variableName);
variablesContainer =
&GetProjectScopedContainers()
.GetVariablesContainersList()
.GetVariablesContainerFromVariableOrPropertyName(
variableName);
} else {
if (GetProjectScopedContainers().GetVariablesContainersList().Has(
variableName)) {

View File

@@ -448,12 +448,10 @@ bool EventsVariableReplacer::DoVisitInstruction(gd::Instruction& instruction,
bool shouldDeleteInstruction = false;
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[&](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
const gd::String& type = parameterMetadata.GetType();
if (!gd::ParameterMetadata::IsExpression("variable", type) &&

View File

@@ -150,7 +150,7 @@ bool ProjectElementRenamer::DoVisitInstruction(gd::Instruction &instruction,
instruction.GetParameters(), metadata.GetParameters(),
[&](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName) {
const gd::String &lastObjectName, size_t lastObjectIndex) {
if (parameterMetadata.GetType() == "layer") {
if (parameterValue.GetPlainString().length() < 2) {
// This is either the base layer or an invalid layer name.

View File

@@ -76,6 +76,7 @@ void ObjectAssetSerializer::SerializeTo(
double width = 0;
double height = 0;
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
if (project.HasEventsBasedObject(object.GetType())) {
SerializerElement &variantsElement =
objectAssetElement.AddChild("variants");
@@ -87,7 +88,6 @@ void ObjectAssetSerializer::SerializeTo(
height = variant->GetAreaMaxY() - variant->GetAreaMinY();
}
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
gd::ObjectAssetSerializer::SerializeUsedVariantsTo(
project, object, variantsElement, alreadyUsedVariantIdentifiers);
}
@@ -114,14 +114,24 @@ void ObjectAssetSerializer::SerializeTo(
resourceElement.SetAttribute("name", resource.GetName());
}
std::unordered_set<gd::String> usedExtensionNames;
usedExtensionNames.insert(extensionName);
for (auto &usedVariantIdentifier : alreadyUsedVariantIdentifiers) {
usedExtensionNames.insert(PlatformExtension::GetExtensionFromFullObjectType(
usedVariantIdentifier));
}
SerializerElement &requiredExtensionsElement =
objectAssetElement.AddChild("requiredExtensions");
requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension");
if (project.HasEventsFunctionsExtensionNamed(extensionName)) {
SerializerElement &requiredExtensionElement =
requiredExtensionsElement.AddChild("requiredExtension");
requiredExtensionElement.SetAttribute("extensionName", extensionName);
requiredExtensionElement.SetAttribute("extensionVersion", "1.0.0");
for (auto &usedExtensionName : usedExtensionNames) {
if (project.HasEventsFunctionsExtensionNamed(usedExtensionName)) {
auto &extension = project.GetEventsFunctionsExtension(usedExtensionName);
SerializerElement &requiredExtensionElement =
requiredExtensionsElement.AddChild("requiredExtension");
requiredExtensionElement.SetAttribute("extensionName", usedExtensionName);
requiredExtensionElement.SetAttribute("extensionVersion",
extension.GetVersion());
}
}
// TODO This can be removed when the asset script no longer require it.

View File

@@ -227,12 +227,11 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi
platform, instruction.GetType());
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(),
metadata.GetParameters(),
[this, &instruction](const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterExpression,
size_t parameterIndex,
const gd::String& lastObjectName) {
instruction.GetParameters(), metadata.GetParameters(),
[this, &instruction](
const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterExpression, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
const String& parameterValue = parameterExpression.GetPlainString();
if (parameterMetadata.GetType() == "fontResource") {
gd::String updatedParameterValue = parameterValue;

View File

@@ -248,12 +248,13 @@ gd::String PropertyFunctionGenerator::GetStringifiedExtraInfo(
gd::String arrayString;
arrayString += "[";
bool isFirst = true;
for (const gd::String &choice : property.GetExtraInfo()) {
for (const auto &choice : property.GetChoices()) {
if (!isFirst) {
arrayString += ",";
}
isFirst = false;
arrayString += "\"" + choice + "\"";
// TODO Handle labels (and search "choice label")
arrayString += "\"" + choice.GetValue() + "\"";
}
arrayString += "]";
return arrayString;

View File

@@ -75,6 +75,17 @@ void ResourceExposer::ExposeProjectResources(
// Expose global objects configuration resources
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
objectWorker.Launch(project.GetObjects());
// Exposed extension event resources
// Note that using resources in extensions is very unlikely and probably not
// worth the effort of something smart.
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
e++) {
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
project, eventsFunctionsExtension, eventWorker);
}
}
void ResourceExposer::ExposeLayoutResources(
@@ -103,16 +114,6 @@ void ResourceExposer::ExposeLayoutResources(
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
gd::ProjectBrowserHelper::ExposeLayoutEventsAndDependencies(
project, layout, eventWorker);
// Exposed extension event resources
// Note that using resources in extensions is very unlikely and probably not
// worth the effort of something smart.
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
e++) {
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
project, eventsFunctionsExtension, eventWorker);
}
}
void ResourceExposer::ExposeEffectResources(

View File

@@ -37,6 +37,7 @@ void EventsBasedObjectVariant::SerializeTo(SerializerElement &element) const {
layers.SerializeLayersTo(element.AddChild("layers"));
initialInstances.SerializeTo(element.AddChild("instances"));
editorSettings.SerializeTo(element.AddChild("editionSettings"));
}
void EventsBasedObjectVariant::UnserializeFrom(
@@ -66,6 +67,7 @@ void EventsBasedObjectVariant::UnserializeFrom(
layers.Reset();
}
initialInstances.UnserializeFrom(element.GetChild("instances"));
editorSettings.UnserializeFrom(element.GetChild("editionSettings"));
}
} // namespace gd

View File

@@ -5,6 +5,7 @@
*/
#pragma once
#include "GDCore/IDE/Dialogs/LayoutEditorCanvas/EditorSettings.h"
#include "GDCore/Project/InitialInstancesContainer.h"
#include "GDCore/Project/LayersContainer.h"
#include "GDCore/Project/ObjectsContainer.h"
@@ -199,6 +200,19 @@ public:
const gd::String &GetAssetStoreOriginalName() const {
return assetStoreOriginalName;
};
/**
*
* \brief Get the user settings for the IDE.
*/
const gd::EditorSettings& GetAssociatedEditorSettings() const {
return editorSettings;
}
/**
* \brief Get the user settings for the IDE.
*/
gd::EditorSettings& GetAssociatedEditorSettings() { return editorSettings; }
void SerializeTo(SerializerElement &element) const;
@@ -224,6 +238,7 @@ private:
* store.
*/
gd::String assetStoreOriginalName;
gd::EditorSettings editorSettings;
};
} // namespace gd

View File

@@ -34,6 +34,20 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
}
}
if (!choices.empty()
// Compatibility with GD <= 5.5.239
|| !extraInformation.empty()
// end of compatibility code
) {
SerializerElement &choicesElement = element.AddChild("choices");
choicesElement.ConsiderAsArrayOf("choice");
for (const auto &choice : choices) {
auto &choiceElement = choicesElement.AddChild("Choice");
choiceElement.SetStringAttribute("value", choice.GetValue());
choiceElement.SetStringAttribute("label", choice.GetLabel());
}
}
if (hidden) {
element.AddChild("hidden").SetBoolValue(hidden);
}
@@ -80,6 +94,26 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
extraInformationElement.GetChild(i).GetStringValue());
}
if (element.HasChild("choices")) {
choices.clear();
const SerializerElement &choicesElement = element.GetChild("choices");
choicesElement.ConsiderAsArrayOf("choice");
for (std::size_t i = 0; i < choicesElement.GetChildrenCount(); ++i) {
auto &choiceElement = choicesElement.GetChild(i);
AddChoice(choiceElement.GetStringAttribute("value"),
choiceElement.GetStringAttribute("label"));
}
}
// Compatibility with GD <= 5.5.239
else if (type == "Choice") {
choices.clear();
for (auto &choiceValue : extraInformation) {
AddChoice(choiceValue, choiceValue);
}
extraInformation.clear();
}
// end of compatibility code
hidden = element.HasChild("hidden")
? element.GetChild("hidden").GetBoolValue()
: false;

View File

@@ -116,6 +116,11 @@ class GD_CORE_API PropertyDescriptor {
return *this;
}
PropertyDescriptor& ClearChoices() {
choices.clear();
return *this;
}
PropertyDescriptor& AddChoice(const gd::String& value,
const gd::String& label) {
choices.push_back(PropertyDescriptorChoice(value, label));

View File

@@ -24,7 +24,7 @@ The rest of this page is an introduction to the main concepts of GDevelop archit
Extensions do have the same distinction between the "**IDE**" part and the "**Runtime**" part. For example, most extensions have:
- A file called [`JsExtension.js`(https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/JsExtension.js)], which contains the _declaration_ of the extension for the **IDE**
- A file called [`JsExtension.js`](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/JsExtension.js), which contains the _declaration_ of the extension for the **IDE**
- One or more files implementing the feature for the game, in other words for **Runtime**. This can be a [Runtime Object](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/dummyruntimeobject.ts) or a [Runtime Behavior](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/dummyruntimebehavior.ts), [functions called by actions or conditions](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/examplejsextensiontools.ts) or by the game engine.
### "Runtime" and "IDE" difference using an example: the `gd::Variable` class

View File

@@ -764,129 +764,6 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
REQUIRE(worker.audios[0] == "res4");
}
SECTION("Can find resource usages in event-based functions") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
project.GetResourcesManager().AddResource(
"res1", "path/to/file1.png", "image");
project.GetResourcesManager().AddResource(
"res2", "path/to/file2.png", "image");
project.GetResourcesManager().AddResource(
"res3", "path/to/file3.png", "image");
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
auto &function = extension.GetEventsFunctions().InsertNewEventsFunction(
"MyFreeFunction", 0);
gd::StandardEvent standardEvent;
gd::Instruction instruction;
instruction.SetType("MyExtension::DoSomethingWithResources");
instruction.SetParametersCount(3);
instruction.SetParameter(0, "res3");
instruction.SetParameter(1, "res1");
instruction.SetParameter(2, "res4");
standardEvent.GetActions().Insert(instruction);
function.GetEvents().InsertEvent(standardEvent);
auto& layout = project.InsertNewLayout("MyScene", 0);
// MyEventExtension::MyFreeFunction doesn't need to be actually used in
// events because the implementation is naive.
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
REQUIRE(worker.bitmapFonts.size() == 1);
REQUIRE(worker.bitmapFonts[0] == "res3");
REQUIRE(worker.images.size() == 1);
REQUIRE(worker.images[0] == "res1");
REQUIRE(worker.audios.size() == 1);
REQUIRE(worker.audios[0] == "res4");
}
SECTION("Can find resource usages in event-based behavior functions") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
project.GetResourcesManager().AddResource(
"res1", "path/to/file1.png", "image");
project.GetResourcesManager().AddResource(
"res2", "path/to/file2.png", "image");
project.GetResourcesManager().AddResource(
"res3", "path/to/file3.png", "image");
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
auto& behavior = extension.GetEventsBasedBehaviors().InsertNew("MyBehavior", 0);
auto& function = behavior.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
gd::StandardEvent standardEvent;
gd::Instruction instruction;
instruction.SetType("MyExtension::DoSomethingWithResources");
instruction.SetParametersCount(3);
instruction.SetParameter(0, "res3");
instruction.SetParameter(1, "res1");
instruction.SetParameter(2, "res4");
standardEvent.GetActions().Insert(instruction);
function.GetEvents().InsertEvent(standardEvent);
auto& layout = project.InsertNewLayout("MyScene", 0);
// MyEventExtension::MyBehavior::MyFunction doesn't need to be actually used in
// events because the implementation is naive.
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
REQUIRE(worker.bitmapFonts.size() == 1);
REQUIRE(worker.bitmapFonts[0] == "res3");
REQUIRE(worker.images.size() == 1);
REQUIRE(worker.images[0] == "res1");
REQUIRE(worker.audios.size() == 1);
REQUIRE(worker.audios[0] == "res4");
}
SECTION("Can find resource usages in event-based object functions") {
gd::Project project;
gd::Platform platform;
SetupProjectWithDummyPlatform(project, platform);
project.GetResourcesManager().AddResource(
"res1", "path/to/file1.png", "image");
project.GetResourcesManager().AddResource(
"res2", "path/to/file2.png", "image");
project.GetResourcesManager().AddResource(
"res3", "path/to/file3.png", "image");
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
auto& object = extension.GetEventsBasedObjects().InsertNew("MyObject", 0);
auto& function = object.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
gd::StandardEvent standardEvent;
gd::Instruction instruction;
instruction.SetType("MyExtension::DoSomethingWithResources");
instruction.SetParametersCount(3);
instruction.SetParameter(0, "res3");
instruction.SetParameter(1, "res1");
instruction.SetParameter(2, "res4");
standardEvent.GetActions().Insert(instruction);
function.GetEvents().InsertEvent(standardEvent);
auto& layout = project.InsertNewLayout("MyScene", 0);
// MyEventExtension::MyObject::MyFunction doesn't need to be actually used in
// events because the implementation is naive.
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
REQUIRE(worker.bitmapFonts.size() == 1);
REQUIRE(worker.bitmapFonts[0] == "res3");
REQUIRE(worker.images.size() == 1);
REQUIRE(worker.images[0] == "res1");
REQUIRE(worker.audios.size() == 1);
REQUIRE(worker.audios[0] == "res4");
}
SECTION("Can find resource usages in layer effects") {
gd::Project project;
gd::Platform platform;

View File

@@ -139,8 +139,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
.SetLabel("Dot shape")
.SetDescription("The shape is used for collision.")
.SetGroup("Movement");
property.GetExtraInfo().push_back("Dot shape");
property.GetExtraInfo().push_back("Bounding disk");
property.AddChoice("DotShape", "Dot shape");
property.AddChoice("BoundingDisk", "Bounding disk");
gd::PropertyFunctionGenerator::GenerateBehaviorGetterAndSetter(
project, extension, behavior, property, false);
@@ -157,7 +157,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
gd::EventsFunction::ExpressionAndCondition);
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
"[\"Dot shape\",\"Bounding disk\"]");
"[\"DotShape\",\"BoundingDisk\"]");
}
SECTION("Can generate functions for a boolean property in a behavior") {
@@ -386,8 +386,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
.SetLabel("Dot shape")
.SetDescription("The shape is used for collision.")
.SetGroup("Movement");
property.GetExtraInfo().push_back("Dot shape");
property.GetExtraInfo().push_back("Bounding disk");
property.AddChoice("DotShape", "Dot shape");
property.AddChoice("BoundingDisk", "Bounding disk");
gd::PropertyFunctionGenerator::GenerateObjectGetterAndSetter(
project, extension, object, property);
@@ -404,7 +404,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
gd::EventsFunction::ExpressionAndCondition);
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
"[\"Dot shape\",\"Bounding disk\"]");
"[\"DotShape\",\"BoundingDisk\"]");
}
SECTION("Can generate functions for a boolean property in an object") {

View File

@@ -97,6 +97,14 @@ namespace gdjs {
oldObjectData: Object3DData,
newObjectData: Object3DData
): boolean {
this.updateOriginalDimensionsFromObjectData(oldObjectData, newObjectData);
return true;
}
updateOriginalDimensionsFromObjectData(
oldObjectData: Object3DData,
newObjectData: Object3DData
): void {
// There is no need to check if they changed because events can't modify them.
this._setOriginalWidth(
getValidDimensionValue(newObjectData.content.width)
@@ -107,12 +115,13 @@ namespace gdjs {
this._setOriginalDepth(
getValidDimensionValue(newObjectData.content.depth)
);
return true;
}
getNetworkSyncData(): Object3DNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Object3DNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
z: this.getZ(),
d: this.getDepth(),
rx: this.getRotationX(),
@@ -123,8 +132,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: Object3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
if (networkSyncData.rx !== undefined)

View File

@@ -452,9 +452,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Cube3DObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Cube3DObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
mt: this._materialType,
fo: this._facesOrientation,
bfu: this._backFaceUpThroughWhichAxisRotation,
@@ -466,9 +468,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: Cube3DObjectNetworkSyncData
networkSyncData: Cube3DObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;

View File

@@ -1,12 +1,16 @@
namespace gdjs {
type CustomObject3DNetworkSyncDataType = CustomObjectNetworkSyncDataType & {
type CustomObject3DNetworkSyncDataType = {
z: float;
d: float;
rx: float;
ry: float;
ifz: boolean;
ccz: float;
};
type CustomObject3DNetworkSyncData = CustomObjectNetworkSyncData &
CustomObject3DNetworkSyncDataType;
/**
* Base class for 3D custom objects.
*/
@@ -85,21 +89,25 @@ namespace gdjs {
}
}
getNetworkSyncData(): CustomObject3DNetworkSyncDataType {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): CustomObject3DNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
z: this.getZ(),
d: this.getDepth(),
rx: this.getRotationX(),
ry: this.getRotationY(),
ifz: this.isFlippedZ(),
ccz: this._customCenterZ,
};
}
updateFromNetworkSyncData(
networkSyncData: CustomObject3DNetworkSyncDataType
networkSyncData: CustomObject3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
if (networkSyncData.rx !== undefined)
@@ -107,6 +115,8 @@ namespace gdjs {
if (networkSyncData.ry !== undefined)
this.setRotationY(networkSyncData.ry);
if (networkSyncData.ifz !== undefined) this.flipZ(networkSyncData.ifz);
if (networkSyncData.ccz !== undefined)
this._customCenterZ = networkSyncData.ccz;
}
/**

View File

@@ -1901,6 +1901,11 @@ module.exports = {
.getOrCreate('density')
.setValue('0.0012')
.setLabel(_('Density'))
.setDescription(
_(
'Density of the fog. Usual values are between 0.0005 (far away) and 0.005 (very thick fog).'
)
)
.setType('number');
}
{
@@ -1908,7 +1913,9 @@ module.exports = {
.addEffect('AmbientLight')
.setFullName(_('Ambient light'))
.setDescription(
_('A light that illuminates all objects from every direction.')
_(
'A light that illuminates all objects from every direction. Often used along with a Directional light (though a Hemisphere light can be used instead of an Ambient light).'
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
@@ -1929,7 +1936,11 @@ module.exports = {
const effect = extension
.addEffect('DirectionalLight')
.setFullName(_('Directional light'))
.setDescription(_('A very far light source like the sun.'))
.setDescription(
_(
"A very far light source like the sun. This is the light to use for casting shadows for 3D objects (other lights won't emit shadows). Often used along with a Hemisphere light."
)
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/DirectionalLight.js');
@@ -2013,7 +2024,7 @@ module.exports = {
.setFullName(_('Hemisphere light'))
.setDescription(
_(
'A light that illuminates objects from every direction with a gradient.'
'A light that illuminates objects from every direction with a gradient. Often used along with a Directional light.'
)
)
.markAsNotWorkingForObjects()
@@ -2057,6 +2068,48 @@ module.exports = {
.setType('number')
.setGroup(_('Orientation'));
}
{
const effect = extension
.addEffect('Skybox')
.setFullName(_('Skybox'))
.setDescription(
_('Display a background on a cube surrounding the scene.')
)
.markAsNotWorkingForObjects()
.markAsOnlyWorkingFor3D()
.addIncludeFile('Extensions/3D/Skybox.js');
const properties = effect.getProperties();
properties
.getOrCreate('rightFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Right face (X+)'));
properties
.getOrCreate('leftFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Left face (X-)'));
properties
.getOrCreate('bottomFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Bottom face (Y+)'));
properties
.getOrCreate('topFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Top face (Y-)'));
properties
.getOrCreate('frontFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Front face (Z+)'));
properties
.getOrCreate('backFaceResourceName')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Back face (Z-)'));
}
{
const effect = extension
.addEffect('HueAndSaturation')

View File

@@ -8,6 +8,7 @@ namespace gdjs {
anis: Model3DAnimation[];
ai: integer;
ass: float;
aet: float;
ap: boolean;
cfd: float;
};
@@ -152,6 +153,14 @@ namespace gdjs {
}
}
override updateOriginalDimensionsFromObjectData(
oldObjectData: Object3DData,
newObjectData: Object3DData
): void {
// Original dimensions must not be reset by `super.updateFromObjectData`.
// `_updateModel` has a different logic to evaluate them using `keepAspectRatio`.
}
updateFromObjectData(
oldObjectData: Model3DObjectData,
newObjectData: Model3DObjectData
@@ -181,8 +190,14 @@ namespace gdjs {
oldObjectData.content.keepAspectRatio !==
newObjectData.content.keepAspectRatio ||
oldObjectData.content.materialType !==
newObjectData.content.materialType
newObjectData.content.materialType ||
oldObjectData.content.centerLocation !==
newObjectData.content.centerLocation
) {
// The center is applied to the model by `_updateModel`.
this._centerPoint = getPointForLocation(
newObjectData.content.centerLocation
);
this._updateModel(newObjectData);
}
if (
@@ -192,14 +207,7 @@ namespace gdjs {
this._originPoint = getPointForLocation(
newObjectData.content.originLocation
);
}
if (
oldObjectData.content.centerLocation !==
newObjectData.content.centerLocation
) {
this._centerPoint = getPointForLocation(
newObjectData.content.centerLocation
);
this._renderer.updatePosition();
}
if (
oldObjectData.content.isCastingShadow !==
@@ -216,24 +224,28 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Model3DObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Model3DObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
mt: this._materialType,
op: this._originPoint,
cp: this._centerPoint,
anis: this._animations,
ai: this._currentAnimationIndex,
ass: this._animationSpeedScale,
aet: this.getAnimationElapsedTime(),
ap: this._animationPaused,
cfd: this._crossfadeDuration,
};
}
updateFromNetworkSyncData(
networkSyncData: Model3DObjectNetworkSyncData
networkSyncData: Model3DObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;
@@ -247,11 +259,14 @@ namespace gdjs {
if (networkSyncData.anis !== undefined) {
this._animations = networkSyncData.anis;
}
if (networkSyncData.ass !== undefined) {
this.setAnimationSpeedScale(networkSyncData.ass);
}
if (networkSyncData.ai !== undefined) {
this.setAnimationIndex(networkSyncData.ai);
}
if (networkSyncData.ass !== undefined) {
this.setAnimationSpeedScale(networkSyncData.ass);
if (networkSyncData.aet !== undefined) {
this.setAnimationElapsedTime(networkSyncData.aet);
}
if (networkSyncData.ap !== undefined) {
if (networkSyncData.ap !== this.isAnimationPaused()) {
@@ -273,14 +288,17 @@ namespace gdjs {
const rotationX = objectData.content.rotationX || 0;
const rotationY = objectData.content.rotationY || 0;
const rotationZ = objectData.content.rotationZ || 0;
const width = objectData.content.width || 100;
const height = objectData.content.height || 100;
const depth = objectData.content.depth || 100;
const keepAspectRatio = objectData.content.keepAspectRatio;
this._renderer._updateModel(
rotationX,
rotationY,
rotationZ,
this._getOriginalWidth(),
this._getOriginalHeight(),
this._getOriginalDepth(),
width,
height,
depth,
keepAspectRatio
);
}

View File

@@ -233,6 +233,10 @@ namespace gdjs {
this._object._setOriginalWidth(scaleRatio * modelWidth);
this._object._setOriginalHeight(scaleRatio * modelHeight);
this._object._setOriginalDepth(scaleRatio * modelDepth);
} else {
this._object._setOriginalWidth(originalWidth);
this._object._setOriginalHeight(originalHeight);
this._object._setOriginalDepth(originalDepth);
}
}
@@ -286,6 +290,7 @@ namespace gdjs {
this.get3DRendererObject().remove(this._threeObject);
this.get3DRendererObject().add(threeObject);
this._threeObject = threeObject;
this.updatePosition();
this._updateShadow();
// Start the current animation on the new 3D object.

102
Extensions/3D/Skybox.ts Normal file
View File

@@ -0,0 +1,102 @@
namespace gdjs {
interface SkyboxFilterNetworkSyncData {}
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::Skybox',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
makeFilter(
target: EffectsTarget,
effectData: EffectData
): gdjs.PixiFiltersTools.Filter {
if (typeof THREE === 'undefined') {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
_cubeTexture: THREE.CubeTexture;
_oldBackground:
| THREE.CubeTexture
| THREE.Texture
| THREE.Color
| null = null;
_isEnabled: boolean = false;
constructor() {
this._cubeTexture = target
.getRuntimeScene()
.getGame()
.getImageManager()
.getThreeCubeTexture(
effectData.stringParameters.rightFaceResourceName,
effectData.stringParameters.leftFaceResourceName,
effectData.stringParameters.topFaceResourceName,
effectData.stringParameters.bottomFaceResourceName,
effectData.stringParameters.frontFaceResourceName,
effectData.stringParameters.backFaceResourceName
);
}
isEnabled(target: EffectsTarget): boolean {
return this._isEnabled;
}
setEnabled(target: EffectsTarget, enabled: boolean): boolean {
if (this._isEnabled === enabled) {
return true;
}
if (enabled) {
return this.applyEffect(target);
} else {
return this.removeEffect(target);
}
}
applyEffect(target: EffectsTarget): boolean {
const scene = target.get3DRendererObject() as
| THREE.Scene
| null
| undefined;
if (!scene) {
return false;
}
// TODO Add a background stack in LayerPixiRenderer to allow
// filters to stack them.
this._oldBackground = scene.background;
scene.background = this._cubeTexture;
if (!scene.environment) {
scene.environment = this._cubeTexture;
}
this._isEnabled = true;
return true;
}
removeEffect(target: EffectsTarget): boolean {
const scene = target.get3DRendererObject() as
| THREE.Scene
| null
| undefined;
if (!scene) {
return false;
}
scene.background = this._oldBackground;
scene.environment = null;
this._isEnabled = false;
return true;
}
updatePreRender(target: gdjs.EffectsTarget): any {}
updateDoubleParameter(parameterName: string, value: number): void {}
getDoubleParameter(parameterName: string): number {
return 0;
}
updateStringParameter(parameterName: string, value: string): void {}
updateColorParameter(parameterName: string, value: number): void {}
getColorParameter(parameterName: string): number {
return 0;
}
updateBooleanParameter(parameterName: string, value: boolean): void {}
getNetworkSyncData(): SkyboxFilterNetworkSyncData {
return {};
}
updateFromNetworkSyncData(
syncData: SkyboxFilterNetworkSyncData
): void {}
})();
}
})()
);
}

View File

@@ -473,7 +473,14 @@ namespace gdjs {
this._parentOldMaxY = instanceContainer.getUnrotatedViewportMaxY();
}
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
// Custom objects can be resized during the events step.
// The anchor constraints must be applied on child-objects after the parent events.
const isChildObject = instanceContainer !== instanceContainer.getScene();
if (isChildObject) {
this.doStepPreEvents(instanceContainer);
}
}
private _convertCoords(
instanceContainer: gdjs.RuntimeInstanceContainer,

View File

@@ -145,9 +145,11 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): BBTextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BBTextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
text: this._text,
o: this._opacity,
c: this._color,
@@ -162,9 +164,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: BBTextObjectNetworkSyncData
networkSyncData: BBTextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (this._text !== undefined) {
this.setBBText(networkSyncData.text);
}

View File

@@ -155,9 +155,11 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): BitmapTextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BitmapTextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
text: this._text,
opa: this._opacity,
tint: this._tint,
@@ -172,9 +174,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: BitmapTextObjectNetworkSyncData
networkSyncData: BitmapTextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (this._text !== undefined) {
this.setText(networkSyncData.text);
}

View File

@@ -21,7 +21,9 @@ module.exports = {
.setExtensionInformation(
'DebuggerTools',
_('Debugger Tools'),
_('Allow to interact with the editor debugger from the game.'),
_(
'Allow to interact with the editor debugger from the game (notably: enable 2D debug draw, log a message in the debugger console).'
),
'Arthur Pacaud (arthuro555), Aurélien Vivet (Bouh)',
'MIT'
)

View File

@@ -12,7 +12,8 @@ This project is released under the MIT License.
#include "GDCore/Tools/Localization.h"
void DestroyOutsideBehavior::InitializeContent(gd::SerializerElement& content) {
content.SetAttribute("extraBorder", 300);
content.SetAttribute("extraBorder", 200);
content.SetAttribute("unseenGraceDistance", 10000);
}
#if defined(GD_IDE_ONLY)
@@ -27,7 +28,15 @@ DestroyOutsideBehavior::GetProperties(
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetLabel(_("Deletion margin"))
.SetDescription(_("Margin before deleting the object, in pixels"));
.SetDescription(_("Margin before deleting the object, in pixels."));
properties["unseenGraceDistance"]
.SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("unseenGraceDistance", 0)))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetLabel(_("Unseen object grace distance"))
.SetDescription(_("If the object hasn't been visible yet, don't delete it until it travels this far beyond the screen (in pixels). Useful to avoid objects being deleted before they are visible when they spawn."));
return properties;
}
@@ -38,6 +47,8 @@ bool DestroyOutsideBehavior::UpdateProperty(
const gd::String& value) {
if (name == "extraBorder")
behaviorContent.SetAttribute("extraBorder", value.To<double>());
else if (name == "unseenGraceDistance")
behaviorContent.SetAttribute("unseenGraceDistance", value.To<double>());
else
return false;

View File

@@ -6,6 +6,7 @@ This project is released under the MIT License.
*/
#include "DestroyOutsideBehavior.h"
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Project/BehaviorsSharedData.h"
#include "GDCore/Tools/Localization.h"
@@ -19,11 +20,10 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
"outside of the bounds of the 2D camera. Useful for 2D bullets or "
"other short-lived objects. Don't use it for 3D objects in a "
"FPS/TPS game or any game with a camera not being a top view "
"(for 3D objects, prefer comparing "
"the position, for example Z position to see if an object goes "
"outside of the bound of the map). Be careful when using this "
"behavior because if the object appears outside of the screen, it "
"will be immediately removed."),
"(for 3D objects, prefer comparing the position, for example Z "
"position to see if an object goes outside of the bound of the "
"map). If the object appears outside of the screen, it's not "
"removed unless it goes beyond the unseen object grace distance."),
"Florian Rival",
"Open source (MIT License)")
.SetCategory("Game mechanic")
@@ -44,34 +44,39 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
std::shared_ptr<gd::BehaviorsSharedData>())
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
aut.AddCondition("ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("Compare the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted."),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png",
"CppPlatform/Extensions/destroyoutsideicon16.png")
aut.AddExpressionAndConditionAndAction(
"number",
"ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted"),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "DestroyOutside")
.UseStandardRelationalOperatorParameters(
"number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced()
.SetFunctionName("GetExtraBorder");
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced();
aut.AddAction("ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("Change the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted."),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png",
"CppPlatform/Extensions/destroyoutsideicon16.png")
// Deprecated:
aut.AddDuplicatedAction("ExtraBorder", "DestroyOutside::SetExtraBorder")
.SetHidden();
aut.AddDuplicatedCondition("ExtraBorder", "DestroyOutside::ExtraBorder")
.SetHidden();
aut.AddExpressionAndConditionAndAction(
"number",
"UnseenGraceDistance",
_("Unseen object grace distance"),
_("the grace distance (in pixels) before deleting the object if it "
"has "
"never been visible on the screen. Useful to avoid objects being "
"deleted before they are visible when they spawn"),
_("the unseen grace distance"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "DestroyOutside")
.UseStandardOperatorParameters("number",
gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced()
.SetFunctionName("SetExtraBorder")
.SetGetter("GetExtraBorder");
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced();
}

View File

@@ -5,11 +5,11 @@ Copyright (c) 2014-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#include <iostream>
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Tools/Localization.h"
#include <iostream>
void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension);
/**
@@ -29,19 +29,36 @@ class DestroyOutsideBehaviorJsExtension : public gd::PlatformExtension {
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
GetAllExpressionsForBehavior(
"DestroyOutsideBehavior::DestroyOutside")["ExtraBorder"]
.SetFunctionName("getExtraBorder");
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::ExtraBorder"]
.SetFunctionName("getExtraBorder");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::SetExtraBorder"]
.SetFunctionName("setExtraBorder")
.SetGetter("getExtraBorder");
// Deprecated:
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::ExtraBorder"]
.SetFunctionName("getExtraBorder")
.SetIncludeFile(
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
.SetFunctionName("getExtraBorder");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::ExtraBorder"]
.SetFunctionName("setExtraBorder")
.SetGetter("getExtraBorder")
.SetIncludeFile(
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
.SetGetter("getExtraBorder");
GetAllExpressionsForBehavior(
"DestroyOutsideBehavior::DestroyOutside")["UnseenGraceDistance"]
.SetFunctionName("getUnseenGraceDistance");
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::UnseenGraceDistance"]
.SetFunctionName("getUnseenGraceDistance");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::SetUnseenGraceDistance"]
.SetFunctionName("setUnseenGraceDistance")
.SetGetter("getUnseenGraceDistance");
GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION();
};

View File

@@ -8,7 +8,9 @@ namespace gdjs {
* The DestroyOutsideRuntimeBehavior represents a behavior that destroys the object when it leaves the screen.
*/
export class DestroyOutsideRuntimeBehavior extends gdjs.RuntimeBehavior {
_extraBorder: any;
_extraBorder: float;
_unseenGraceDistance: float;
_hasBeenOnScreen: boolean;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -17,12 +19,20 @@ namespace gdjs {
) {
super(instanceContainer, behaviorData, owner);
this._extraBorder = behaviorData.extraBorder || 0;
this._unseenGraceDistance = behaviorData.unseenGraceDistance || 0;
this._hasBeenOnScreen = false;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
if (oldBehaviorData.extraBorder !== newBehaviorData.extraBorder) {
this._extraBorder = newBehaviorData.extraBorder;
}
if (
oldBehaviorData.unseenGraceDistance !==
newBehaviorData.unseenGraceDistance
) {
this._unseenGraceDistance = newBehaviorData.unseenGraceDistance;
}
return true;
}
@@ -35,23 +45,47 @@ namespace gdjs {
const ocy = this.owner.getDrawableY() + this.owner.getCenterY();
const layer = instanceContainer.getLayer(this.owner.getLayer());
const boundingCircleRadius = Math.sqrt(ow * ow + oh * oh) / 2.0;
const cameraLeft = layer.getCameraX() - layer.getCameraWidth() / 2;
const cameraRight = layer.getCameraX() + layer.getCameraWidth() / 2;
const cameraTop = layer.getCameraY() - layer.getCameraHeight() / 2;
const cameraBottom = layer.getCameraY() + layer.getCameraHeight() / 2;
if (
ocx + boundingCircleRadius + this._extraBorder <
layer.getCameraX() - layer.getCameraWidth() / 2 ||
ocx - boundingCircleRadius - this._extraBorder >
layer.getCameraX() + layer.getCameraWidth() / 2 ||
ocy + boundingCircleRadius + this._extraBorder <
layer.getCameraY() - layer.getCameraHeight() / 2 ||
ocy - boundingCircleRadius - this._extraBorder >
layer.getCameraY() + layer.getCameraHeight() / 2
ocx + boundingCircleRadius + this._extraBorder < cameraLeft ||
ocx - boundingCircleRadius - this._extraBorder > cameraRight ||
ocy + boundingCircleRadius + this._extraBorder < cameraTop ||
ocy - boundingCircleRadius - this._extraBorder > cameraBottom
) {
//We are outside the camera area.
this.owner.deleteFromScene();
if (this._hasBeenOnScreen) {
// Object is outside the camera area and object was previously seen inside it:
// delete it now.
this.owner.deleteFromScene();
} else if (
ocx + boundingCircleRadius + this._unseenGraceDistance < cameraLeft ||
ocx - boundingCircleRadius - this._unseenGraceDistance >
cameraRight ||
ocy + boundingCircleRadius + this._unseenGraceDistance < cameraTop ||
ocy - boundingCircleRadius - this._unseenGraceDistance > cameraBottom
) {
// Object is outside the camera area and also outside the grace distance:
// force deletion.
this.owner.deleteFromScene();
} else {
// Object is outside the camera area but inside the grace distance
// and was never seen inside the camera area: don't delete it yet.
}
} else {
this._hasBeenOnScreen = true;
}
}
/**
* Set an additional border to the camera viewport as a buffer before the object gets destroyed.
* Set the additional border outside the camera area.
*
* If the object goes beyond the camera area and this border, it will be deleted (unless it was
* never seen inside the camera area and this border before, in which case it will be deleted
* according to the grace distance).
* @param val Border in pixels.
*/
setExtraBorder(val: number): void {
@@ -59,12 +93,36 @@ namespace gdjs {
}
/**
* Get the additional border of the camera viewport buffer which triggers the destruction of an object.
* Get the additional border outside the camera area.
* @return The additional border around the camera viewport in pixels
*/
getExtraBorder(): number {
return this._extraBorder;
}
/**
* Change the grace distance before an object is deleted if it's outside the camera area
* and was never seen inside the camera area. Typically useful to avoid objects being deleted
* before they are visible when they spawn.
*/
setUnseenGraceDistance(val: number): void {
this._unseenGraceDistance = val;
}
/**
* Get the grace distance before an object is deleted if it's outside the camera area
* and was never seen inside the camera area.
*/
getUnseenGraceDistance(): number {
return this._unseenGraceDistance;
}
/**
* Check if this object has been visible on screen (precisely: inside the camera area *including* the extra border).
*/
hasBeenOnScreen(): boolean {
return this._hasBeenOnScreen;
}
}
gdjs.registerBehavior(
'DestroyOutsideBehavior::DestroyOutside',

View File

@@ -33,6 +33,49 @@ namespace gdjs {
claimSecret?: string;
};
// Rolling window rate limiting
// Implements rate limiting to prevent abuse:
// - Maximum 12 successful successful entries per minute across all leaderboards
// - Maximum 6 successful successful entries per minute per individual leaderboard
// - Works in addition to existing 500ms cooldown between entry tentatives
let _successfulEntriesGlobal: number[] = []; // Timestamps of successful entries across all leaderboards
const GLOBAL_RATE_LIMIT_COUNT = 12;
const PER_LEADERBOARD_RATE_LIMIT_COUNT = 6;
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute in milliseconds
/**
* Clean old entries from the rolling window (older than 1 minute)
*/
const cleanOldEntries = (
entries: number[],
currentTime: number
): number[] => {
return entries.filter(
(timestamp) => currentTime - timestamp < RATE_LIMIT_WINDOW_MS
);
};
/**
* Check if adding a new entry would exceed global rate limits.
*/
const wouldExceedGlobalSuccessRateLimit = (): boolean => {
const currentTime = Date.now();
_successfulEntriesGlobal = cleanOldEntries(
_successfulEntriesGlobal,
currentTime
);
return _successfulEntriesGlobal.length >= GLOBAL_RATE_LIMIT_COUNT;
};
/**
* Record a successful entry for global rate limiting tracking.
*/
const recordGlobalSuccessfulEntry = (): void => {
const currentTime = Date.now();
_successfulEntriesGlobal.push(currentTime);
};
/**
* Hold the state of the save of a score for a leaderboard.
*/
@@ -43,6 +86,9 @@ namespace gdjs {
/** The promise that will be resolved when the score saving is done (successfully or not). */
lastSavingPromise: Promise<void> | null = null;
/** Timestamps of successful entries for this leaderboard (for rate limiting) */
private _successfulEntries: number[] = [];
// Score that is being saved:
private _currentlySavingScore: number | null = null;
private _currentlySavingPlayerName: string | null = null;
@@ -107,13 +153,36 @@ namespace gdjs {
);
}
private _isTooSoonToSaveAnotherScore(): boolean {
private _wouldExceedPerLeaderboardTentativeRateLimit(): boolean {
// Prevent entries within 500ms of each other (per leaderboard)
// as this would indicate surely a score saved every frame.
//
// Note that is on lastScoreSavingStartedAt, not lastScoreSavingSucceededAt,
// which means we limit tentatives here (and not successes).
return (
!!this.lastScoreSavingStartedAt &&
Date.now() - this.lastScoreSavingStartedAt < 500
);
}
private _wouldExceedPerLeaderboardSuccessRateLimit(): boolean {
const currentTime = Date.now();
this._successfulEntries = cleanOldEntries(
this._successfulEntries,
currentTime
);
return (
this._successfulEntries.length >= PER_LEADERBOARD_RATE_LIMIT_COUNT
);
}
private _recordPerLeaderboardAndGlobalSuccessfulEntry(): void {
const currentTime = Date.now();
this._successfulEntries.push(currentTime);
recordGlobalSuccessfulEntry();
}
startSaving({
playerName,
playerId,
@@ -141,7 +210,7 @@ namespace gdjs {
throw new Error('Ignoring this saving request.');
}
if (this._isTooSoonToSaveAnotherScore()) {
if (this._wouldExceedPerLeaderboardTentativeRateLimit()) {
logger.warn(
'Last entry was sent too little time ago. Ignoring this one.'
);
@@ -154,6 +223,24 @@ namespace gdjs {
throw new Error('Ignoring this saving request.');
}
// Rolling window rate limiting check for successful entries.
if (wouldExceedGlobalSuccessRateLimit()) {
logger.warn(
'Rate limit exceeded. Too many entries have been successfully sent recently across all leaderboards. Ignoring this one.'
);
this._setError('GLOBAL_RATE_LIMIT_EXCEEDED');
throw new Error('Ignoring this saving request.');
}
if (this._wouldExceedPerLeaderboardSuccessRateLimit()) {
logger.warn(
'Rate limit exceeded. Too many entries have been successfully sent recently for this leaderboard. Ignoring this one.'
);
this._setError('LEADERBOARD_RATE_LIMIT_EXCEEDED');
throw new Error('Ignoring this saving request.');
}
let resolveSavingPromise: () => void;
const savingPromise = new Promise<void>((resolve) => {
resolveSavingPromise = resolve;
@@ -169,6 +256,9 @@ namespace gdjs {
return {
closeSaving: (leaderboardEntry) => {
// Record successful entry for rolling window rate limiting.
this._recordPerLeaderboardAndGlobalSuccessfulEntry();
if (savingPromise !== this.lastSavingPromise) {
logger.info(
'Score saving result received, but another save was launched in the meantime - ignoring the result of this one.'
@@ -396,7 +486,10 @@ namespace gdjs {
try {
const { closeSaving, closeSavingWithError } =
scoreSavingState.startSaving({ playerName, score });
scoreSavingState.startSaving({
playerName,
score,
});
try {
const leaderboardEntry = await saveScore({
@@ -440,7 +533,10 @@ namespace gdjs {
try {
const { closeSaving, closeSavingWithError } =
scoreSavingState.startSaving({ playerId, score });
scoreSavingState.startSaving({
playerId,
score,
});
try {
const leaderboardEntryId = await saveScore({

View File

@@ -87,16 +87,21 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): LightNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): LightNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
rad: this.getRadius(),
col: this.getColor(),
};
}
updateFromNetworkSyncData(networkSyncData: LightNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: LightNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.rad !== undefined) {
this.setRadius(networkSyncData.rad);

View File

@@ -729,7 +729,9 @@ namespace gdjs {
behavior.playerNumber = ownerPlayerNumber;
}
instance.updateFromNetworkSyncData(messageData);
instance.updateFromNetworkSyncData(messageData, {
clearInputs: false,
});
setLastClockReceivedForInstanceOnScene({
sceneNetworkId,
@@ -1737,7 +1739,7 @@ namespace gdjs {
return;
}
runtimeScene.updateFromNetworkSyncData(messageData);
runtimeScene.updateFromNetworkSyncData(messageData, {});
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
@@ -1890,7 +1892,7 @@ namespace gdjs {
const messageData = message.getData();
const messageSender = message.getSender();
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
runtimeScene.getGame().updateFromNetworkSyncData(messageData, {});
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
@@ -1918,7 +1920,7 @@ namespace gdjs {
// Reapply the game saved updates.
lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {
debugLogger.info(`Reapplying saved update of game.`);
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
runtimeScene.getGame().updateFromNetworkSyncData(messageData, {});
});
// Game updates are always applied properly, so we can clear them.
lastReceivedGameSyncDataUpdates.clear();
@@ -1937,7 +1939,7 @@ namespace gdjs {
debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);
runtimeScene.updateFromNetworkSyncData(messageData);
runtimeScene.updateFromNetworkSyncData(messageData, {});
// We only remove the message if it was successfully applied, so it can be reapplied later,
// in case we were not on the right scene.
lastReceivedSceneSyncDataUpdates.remove(messageData);

View File

@@ -278,7 +278,7 @@ namespace gdjs {
const instanceNetworkId = this._getOrCreateInstanceNetworkId();
const objectName = this.owner.getName();
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const objectNetworkSyncData = this.owner.getNetworkSyncData({});
// this._logToConsoleWithThrottle(
// `Synchronizing object ${this.owner.getName()} (instance ${
@@ -448,7 +448,7 @@ namespace gdjs {
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
objectNetworkSyncData: this.owner.getNetworkSyncData(),
objectNetworkSyncData: this.owner.getNetworkSyncData({}),
sceneNetworkId,
});
this._sendDataToPeersWithIncreasedClock(
@@ -598,7 +598,7 @@ namespace gdjs {
debugLogger.info(
'Sending update message to move the object immediately.'
);
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const objectNetworkSyncData = this.owner.getNetworkSyncData({});
const {
messageName: updateMessageName,
messageData: updateMessageData,

View File

@@ -119,18 +119,21 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): PanelSpriteNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PanelSpriteNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this.getOpacity(),
color: this.getColor(),
};
}
updateFromNetworkSyncData(
networkSyncData: PanelSpriteNetworkSyncData
networkSyncData: PanelSpriteNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -370,9 +370,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): ParticleEmitterObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): ParticleEmitterObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
prms: this.particleRotationMinSpeed,
prmx: this.particleRotationMaxSpeed,
mpc: this.maxParticlesCount,
@@ -399,9 +401,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
syncData: ParticleEmitterObjectNetworkSyncData
syncData: ParticleEmitterObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData);
super.updateFromNetworkSyncData(syncData, options);
if (syncData.x !== undefined) {
this.setX(syncData.x);
}

View File

@@ -6,7 +6,7 @@ namespace gdjs {
const logger = new gdjs.Logger('Pathfinding behavior');
interface PathfindingNetworkSyncDataType {
// Syncing the path should be enough to have a good prediction.
// Syncing the path and its position on it should be enough to have a good prediction.
path: FloatPoint[];
pf: boolean;
sp: number;
@@ -15,6 +15,7 @@ namespace gdjs {
tss: number;
re: boolean;
ma: number;
dos: number;
}
export interface PathfindingNetworkSyncData extends BehaviorNetworkSyncData {
@@ -133,9 +134,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): PathfindingNetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): PathfindingNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
path: this._path,
pf: this._pathFound,
@@ -145,14 +148,16 @@ namespace gdjs {
tss: this._totalSegmentDistance,
re: this._reachedEnd,
ma: this._movementAngle,
dos: this._distanceOnSegment,
},
};
}
updateFromNetworkSyncData(
networkSyncData: PathfindingNetworkSyncData
networkSyncData: PathfindingNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.path !== undefined) {
this._path = behaviorSpecificProps.path;
@@ -181,6 +186,9 @@ namespace gdjs {
if (behaviorSpecificProps.ma !== undefined) {
this._movementAngle = behaviorSpecificProps.ma;
}
if (behaviorSpecificProps.dos !== undefined) {
this._distanceOnSegment = behaviorSpecificProps.dos;
}
}
setCellWidth(width: float): void {

View File

@@ -21,7 +21,11 @@ module.exports = {
.setExtensionInformation(
'Physics2',
_('2D Physics Engine'),
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.",
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.\n" +
'\n' +
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
'\n' +
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 2D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
'Florian Rival, Franco Maciel',
'MIT'
)
@@ -527,6 +531,7 @@ module.exports = {
physics2Behavior,
sharedData
)
.markAsIrrelevantForChildObjects()
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
.addIncludeFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js')
.addRequiredFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm')

View File

@@ -499,7 +499,9 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Physics2NetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): Physics2NetworkSyncData {
const bodyProps = this._body
? {
tpx: this._body.GetTransform().get_p().get_x(),
@@ -520,7 +522,7 @@ namespace gdjs {
aw: undefined,
};
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
...bodyProps,
layers: this.layers,
@@ -529,45 +531,13 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: Physics2NetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: Physics2NetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (
behaviorSpecificProps.tpx !== undefined &&
behaviorSpecificProps.tpy !== undefined &&
behaviorSpecificProps.tqa !== undefined
) {
if (this._body) {
this._body.SetTransform(
this.b2Vec2(behaviorSpecificProps.tpx, behaviorSpecificProps.tpy),
behaviorSpecificProps.tqa
);
}
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined
) {
if (this._body) {
this._body.SetLinearVelocity(
this.b2Vec2(behaviorSpecificProps.lvx, behaviorSpecificProps.lvy)
);
}
}
if (behaviorSpecificProps.av !== undefined) {
if (this._body) {
this._body.SetAngularVelocity(behaviorSpecificProps.av);
}
}
if (behaviorSpecificProps.aw !== undefined) {
if (this._body) {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
@@ -576,6 +546,38 @@ namespace gdjs {
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
}
this.updateBodyFromObject();
if (!this._body) return;
if (
behaviorSpecificProps.tpx !== undefined &&
behaviorSpecificProps.tpy !== undefined &&
behaviorSpecificProps.tqa !== undefined
) {
this._body.SetTransform(
this.b2Vec2(behaviorSpecificProps.tpx, behaviorSpecificProps.tpy),
behaviorSpecificProps.tqa
);
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined
) {
this._body.SetLinearVelocity(
this.b2Vec2(behaviorSpecificProps.lvx, behaviorSpecificProps.lvy)
);
}
if (behaviorSpecificProps.av !== undefined) {
this._body.SetAngularVelocity(behaviorSpecificProps.av);
}
if (behaviorSpecificProps.aw !== undefined) {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
onDeActivate() {

View File

@@ -21,7 +21,11 @@ module.exports = {
.setExtensionInformation(
'Physics3D',
_('3D physics engine'),
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.",
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.\n" +
'\n' +
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
'\n' +
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 3D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
'Florian Rival',
'MIT'
)
@@ -675,6 +679,7 @@ module.exports = {
behavior,
sharedData
)
.markAsIrrelevantForChildObjects()
.addIncludeFile(
'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js'
)
@@ -2043,7 +2048,12 @@ module.exports = {
'PhysicsCharacter3D',
_('3D physics character'),
'PhysicsCharacter3D',
_('Jump and run on platforms.'),
_(
'Allow an object to jump and run on platforms that have the 3D physics behavior' +
'(and which are generally set to "Static" as type, unless the platform is animated/moved in events).\n' +
'\n' +
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
),
'',
'JsPlatform/Extensions/physics_character3d.svg',
'PhysicsCharacter3D',
@@ -2612,7 +2622,7 @@ module.exports = {
'JumpSustainTime',
_('Jump sustain time'),
_(
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained.'
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained'
),
_('the jump sustain time'),
_('Character configuration'),
@@ -3300,7 +3310,11 @@ module.exports = {
'PhysicsCar3D',
_('3D physics car'),
'PhysicsCar3D',
_('Simulate a realistic car using the 3D physics engine.'),
_(
"Simulate a realistic car using the 3D physics engine. This is mostly useful for the car controlled by the player (it's usually too complex for other cars in a game).\n" +
'\n' +
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
),
'',
'JsPlatform/Extensions/physics_car3d.svg',
'PhysicsCar3D',

View File

@@ -495,7 +495,9 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): Physics3DNetworkSyncData {
override getNetworkSyncData(
options: GetNetworkSyncDataOptions
): Physics3DNetworkSyncData {
let bodyProps;
if (this._body) {
const position = this._body.GetPosition();
@@ -537,7 +539,7 @@ namespace gdjs {
};
}
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
...bodyProps,
layers: this.layers,
@@ -547,27 +549,40 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: Physics3DNetworkSyncData
networkSyncData: Physics3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
}
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
}
this._needToRecreateShape = true;
this._needToRecreateBody = true;
this.updateBodyFromObject();
if (!this._body) return;
if (
behaviorSpecificProps.px !== undefined &&
behaviorSpecificProps.py !== undefined &&
behaviorSpecificProps.pz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetPosition(
this._body.GetID(),
this.getRVec3(
behaviorSpecificProps.px,
behaviorSpecificProps.py,
behaviorSpecificProps.pz
),
Jolt.EActivation_DontActivate
);
}
this._sharedData.bodyInterface.SetPosition(
this._body.GetID(),
this.getRVec3(
behaviorSpecificProps.px,
behaviorSpecificProps.py,
behaviorSpecificProps.pz
),
Jolt.EActivation_DontActivate
);
}
if (
behaviorSpecificProps.rx !== undefined &&
@@ -575,56 +590,44 @@ namespace gdjs {
behaviorSpecificProps.rz !== undefined &&
behaviorSpecificProps.rw !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetRotation(
this._body.GetID(),
this.getQuat(
behaviorSpecificProps.rx,
behaviorSpecificProps.ry,
behaviorSpecificProps.rz,
behaviorSpecificProps.rw
),
Jolt.EActivation_DontActivate
);
}
this._sharedData.bodyInterface.SetRotation(
this._body.GetID(),
this.getQuat(
behaviorSpecificProps.rx,
behaviorSpecificProps.ry,
behaviorSpecificProps.rz,
behaviorSpecificProps.rw
),
Jolt.EActivation_DontActivate
);
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined &&
behaviorSpecificProps.lvz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetLinearVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.lvx,
behaviorSpecificProps.lvy,
behaviorSpecificProps.lvz
)
);
}
this._sharedData.bodyInterface.SetLinearVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.lvx,
behaviorSpecificProps.lvy,
behaviorSpecificProps.lvz
)
);
}
if (
behaviorSpecificProps.avx !== undefined &&
behaviorSpecificProps.avy !== undefined &&
behaviorSpecificProps.avz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetAngularVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.avx,
behaviorSpecificProps.avy,
behaviorSpecificProps.avz
)
);
}
}
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
}
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
this._sharedData.bodyInterface.SetAngularVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.avx,
behaviorSpecificProps.avy,
behaviorSpecificProps.avz
)
);
}
}
@@ -921,31 +924,58 @@ namespace gdjs {
this.updateBodyFromObject();
}
recreateBody() {
if (!this._body) {
this._createBody();
return;
}
recreateBody(previousBodyData?: {
linearVelocityX: float;
linearVelocityY: float;
linearVelocityZ: float;
angularVelocityX: float;
angularVelocityY: float;
angularVelocityZ: float;
}) {
const bodyInterface = this._sharedData.bodyInterface;
const linearVelocity = this._body.GetLinearVelocity();
const linearVelocityX = linearVelocity.GetX();
const linearVelocityY = linearVelocity.GetY();
const linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = this._body.GetAngularVelocity();
const angularVelocityX = angularVelocity.GetX();
const angularVelocityY = angularVelocity.GetY();
const angularVelocityZ = angularVelocity.GetZ();
const linearVelocityX = previousBodyData
? previousBodyData.linearVelocityX
: this._body
? this._body.GetLinearVelocity().GetX()
: 0;
const linearVelocityY = previousBodyData
? previousBodyData.linearVelocityY
: this._body
? this._body.GetLinearVelocity().GetY()
: 0;
const linearVelocityZ = previousBodyData
? previousBodyData.linearVelocityZ
: this._body
? this._body.GetLinearVelocity().GetZ()
: 0;
const angularVelocityX = previousBodyData
? previousBodyData.angularVelocityX
: this._body
? this._body.GetAngularVelocity().GetX()
: 0;
const angularVelocityY = previousBodyData
? previousBodyData.angularVelocityY
: this._body
? this._body.GetAngularVelocity().GetY()
: 0;
const angularVelocityZ = previousBodyData
? previousBodyData.angularVelocityZ
: this._body
? this._body.GetAngularVelocity().GetZ()
: 0;
this.bodyUpdater.destroyBody();
this._contactsEndedThisFrame.length = 0;
this._contactsStartedThisFrame.length = 0;
this._currentContacts.length = 0;
if (this._body) {
this.bodyUpdater.destroyBody();
this._contactsEndedThisFrame.length = 0;
this._contactsStartedThisFrame.length = 0;
this._currentContacts.length = 0;
}
this._createBody();
if (!this._body) {
return;
}
const bodyID = this._body.GetID();
bodyInterface.SetLinearVelocity(
bodyID,

View File

@@ -12,6 +12,7 @@ namespace gdjs {
etm: float;
esm: float;
ei: float;
es: float;
}
export interface PhysicsCar3DNetworkSyncData extends BehaviorNetworkSyncData {
@@ -96,7 +97,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
private _dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -168,12 +169,37 @@ namespace gdjs {
this._isHookedToPhysicsStep = true;
}
// Destroy the body before switching the bodyUpdater,
// to ensure the body of the previous bodyUpdater is not left alive.
// (would be a memory leak and would create a phantom body in the physics world)
// But transfer the linear and angular velocity to the new body,
// so the body doesn't stop when it is recreated.
let previousBodyData = {
linearVelocityX: 0,
linearVelocityY: 0,
linearVelocityZ: 0,
angularVelocityX: 0,
angularVelocityY: 0,
angularVelocityZ: 0,
};
if (behavior._body) {
const linearVelocity = behavior._body.GetLinearVelocity();
previousBodyData.linearVelocityX = linearVelocity.GetX();
previousBodyData.linearVelocityY = linearVelocity.GetY();
previousBodyData.linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = behavior._body.GetAngularVelocity();
previousBodyData.angularVelocityX = angularVelocity.GetX();
previousBodyData.angularVelocityY = angularVelocity.GetY();
previousBodyData.angularVelocityZ = angularVelocity.GetZ();
behavior.bodyUpdater.destroyBody();
}
behavior.bodyUpdater =
new gdjs.PhysicsCar3DRuntimeBehavior.VehicleBodyUpdater(
this,
behavior.bodyUpdater
);
behavior.recreateBody();
behavior.recreateBody(previousBodyData);
return this._physics3D;
}
@@ -273,13 +299,15 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): PhysicsCar3DNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PhysicsCar3DNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
lek: this._wasLeftKeyPressed,
rik: this._wasRightKeyPressed,
@@ -291,14 +319,16 @@ namespace gdjs {
etm: this._engineTorqueMax,
esm: this._engineSpeedMax,
ei: this._engineInertia,
es: this.getEngineSpeed(),
},
};
}
override updateFromNetworkSyncData(
networkSyncData: PhysicsCar3DNetworkSyncData
networkSyncData: PhysicsCar3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
this._hasPressedForwardKey = behaviorSpecificProps.upk;
@@ -311,9 +341,15 @@ namespace gdjs {
this._engineTorqueMax = behaviorSpecificProps.etm;
this._engineSpeedMax = behaviorSpecificProps.esm;
this._engineInertia = behaviorSpecificProps.ei;
if (this._vehicleController) {
this._vehicleController
.GetEngine()
.SetCurrentRPM(behaviorSpecificProps.es);
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// When the object is synchronized from the network, the inputs must not be cleared,
// except if asked specifically.
this._clearInputsBetweenFrames = !!options.clearInputs;
}
_getPhysicsPosition(result: Jolt.RVec3): Jolt.RVec3 {
@@ -490,7 +526,7 @@ namespace gdjs {
this._previousAcceleratorStickForce = this._acceleratorStickForce;
this._previousSteeringStickForce = this._steeringStickForce;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._hasPressedForwardKey = false;
this._hasPressedBackwardKey = false;
this._hasPressedRightKey = false;

View File

@@ -2,11 +2,24 @@
namespace gdjs {
interface PhysicsCharacter3DNetworkSyncDataType {
sma: float;
shm: float;
grav: float;
mfs: float;
facc: float;
fdec: float;
fsm: float;
sacc: float;
sdec: float;
ssm: float;
jumpspeed: float;
jumpsustime: float;
sbpa: boolean;
fwa: float;
fws: float;
sws: float;
fs: float;
js: float;
cfs: float;
cjs: float;
cj: boolean;
lek: boolean;
rik: boolean;
@@ -102,7 +115,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
private _dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
/**
* A very small value compare to 1 pixel, yet very huge compare to rounding errors.
@@ -207,10 +220,35 @@ namespace gdjs {
this._isHookedToPhysicsStep = true;
}
// Destroy the body before switching the bodyUpdater,
// to ensure the body of the previous bodyUpdater is not left alive.
// (would be a memory leak and would create a phantom body in the physics world)
// But transfer the linear and angular velocity to the new body,
// so the body doesn't stop when it is recreated.
let previousBodyData = {
linearVelocityX: 0,
linearVelocityY: 0,
linearVelocityZ: 0,
angularVelocityX: 0,
angularVelocityY: 0,
angularVelocityZ: 0,
};
if (behavior._body) {
const linearVelocity = behavior._body.GetLinearVelocity();
previousBodyData.linearVelocityX = linearVelocity.GetX();
previousBodyData.linearVelocityY = linearVelocity.GetY();
previousBodyData.linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = behavior._body.GetAngularVelocity();
previousBodyData.angularVelocityX = angularVelocity.GetX();
previousBodyData.angularVelocityY = angularVelocity.GetY();
previousBodyData.angularVelocityZ = angularVelocity.GetZ();
behavior.bodyUpdater.destroyBody();
}
behavior.bodyUpdater =
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterBodyUpdater(this);
behavior.collisionChecker = this.collisionChecker;
behavior.recreateBody();
behavior.recreateBody(previousBodyData);
// Always begin in the direction of the object.
this._forwardAngle = this.owner.getAngle();
@@ -277,19 +315,34 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): PhysicsCharacter3DNetworkSyncData {
override getNetworkSyncData(
options: GetNetworkSyncDataOptions
): PhysicsCharacter3DNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
sma: this._slopeMaxAngle,
shm: this._stairHeightMax,
grav: this._gravity,
mfs: this._maxFallingSpeed,
facc: this._forwardAcceleration,
fdec: this._forwardDeceleration,
fsm: this._forwardSpeedMax,
sacc: this._sidewaysAcceleration,
sdec: this._sidewaysDeceleration,
ssm: this._sidewaysSpeedMax,
jumpspeed: this._jumpSpeed,
jumpsustime: this._jumpSustainTime,
fwa: this._forwardAngle,
sbpa: this._shouldBindObjectAndForwardAngle,
fws: this._currentForwardSpeed,
sws: this._currentSidewaysSpeed,
fs: this._currentFallSpeed,
js: this._currentJumpSpeed,
cfs: this._currentFallSpeed,
cjs: this._currentJumpSpeed,
cj: this._canJump,
lek: this._wasLeftKeyPressed,
rik: this._wasRightKeyPressed,
@@ -306,16 +359,30 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: PhysicsCharacter3DNetworkSyncData
networkSyncData: PhysicsCharacter3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
this._slopeMaxAngle = behaviorSpecificProps.sma;
this._stairHeightMax = behaviorSpecificProps.shm;
this._gravity = behaviorSpecificProps.grav;
this._maxFallingSpeed = behaviorSpecificProps.mfs;
this._forwardAcceleration = behaviorSpecificProps.facc;
this._forwardDeceleration = behaviorSpecificProps.fdec;
this._forwardSpeedMax = behaviorSpecificProps.fsm;
this._sidewaysAcceleration = behaviorSpecificProps.sacc;
this._sidewaysDeceleration = behaviorSpecificProps.sdec;
this._sidewaysSpeedMax = behaviorSpecificProps.ssm;
this._jumpSpeed = behaviorSpecificProps.jumpspeed;
this._jumpSustainTime = behaviorSpecificProps.jumpsustime;
this._forwardAngle = behaviorSpecificProps.fwa;
this._shouldBindObjectAndForwardAngle = behaviorSpecificProps.sbpa;
this._currentForwardSpeed = behaviorSpecificProps.fws;
this._currentSidewaysSpeed = behaviorSpecificProps.sws;
this._currentFallSpeed = behaviorSpecificProps.fs;
this._currentJumpSpeed = behaviorSpecificProps.js;
this._currentFallSpeed = behaviorSpecificProps.cfs;
this._currentJumpSpeed = behaviorSpecificProps.cjs;
this._canJump = behaviorSpecificProps.cj;
this._hasPressedForwardKey = behaviorSpecificProps.upk;
this._hasPressedBackwardKey = behaviorSpecificProps.dok;
@@ -328,8 +395,8 @@ namespace gdjs {
this._timeSinceCurrentJumpStart = behaviorSpecificProps.tscjs;
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
}
_getPhysicsPosition(result: Jolt.RVec3): Jolt.RVec3 {
@@ -650,7 +717,7 @@ namespace gdjs {
this._wasJumpKeyPressed = this._hasPressedJumpKey;
this._wasStickUsed = this._hasUsedStick;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._hasPressedForwardKey = false;
this._hasPressedBackwardKey = false;
this._hasPressedRightKey = false;

View File

@@ -37,7 +37,8 @@ void DeclarePhysicsBehaviorExtension(gd::PlatformExtension& extension) {
"res/physics-deprecated32.png",
"PhysicsBehavior",
std::make_shared<PhysicsBehavior>(),
std::make_shared<ScenePhysicsDatas>());
std::make_shared<ScenePhysicsDatas>())
.MarkAsIrrelevantForChildObjects();
aut.AddAction("SetStatic",
("Make the object static"),

View File

@@ -147,7 +147,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
_dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
// This is useful when the object is synchronized over the network,
// object is controlled by the network and we want to ensure the current player
// cannot control it.
@@ -227,14 +227,16 @@ namespace gdjs {
this._state = this._falling;
}
getNetworkSyncData(): PlatformerObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PlatformerObjectNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
this._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
cs: this._currentSpeed,
@@ -263,11 +265,52 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: PlatformerObjectNetworkSyncData
networkSyncData: PlatformerObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
switch (behaviorSpecificProps.sn) {
case 'Falling':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setFalling();
}
this._falling.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'OnFloor':
// Let it handle automatically as we don't know which platform to land on.
// @ts-ignore - we assume it's OnFloorStateNetworkSyncData
this._onFloor.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'Jumping':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setJumping();
}
// @ts-ignore - we assume it's JumpingStateNetworkSyncData
this._jumping.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'GrabbingPlatform':
// Let it handle automatically as we don't know which platform to grab.
this._grabbingPlatform.updateFromNetworkSyncData(
// @ts-ignore - we assume it's GrabbingPlatformStateNetworkSyncData
behaviorSpecificProps.ssd
);
break;
case 'OnLadder':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setOnLadder();
}
this._onLadder.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
default:
console.error(
'Unknown state name: ' + behaviorSpecificProps.sn + '.'
);
break;
}
if (behaviorSpecificProps.cs !== this._currentSpeed) {
this._currentSpeed = behaviorSpecificProps.cs;
}
@@ -317,39 +360,10 @@ namespace gdjs {
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
}
if (behaviorSpecificProps.sn !== this._state.toString()) {
switch (behaviorSpecificProps.sn) {
case 'Falling':
this._setFalling();
break;
case 'OnFloor':
// Let it handle automatically as we don't know which platform to land on.
break;
case 'Jumping':
this._setJumping();
break;
case 'GrabbingPlatform':
// Let it handle automatically as we don't know which platform to grab.
break;
case 'OnLadder':
this._setOnLadder();
break;
default:
console.error(
'Unknown state name: ' + behaviorSpecificProps.sn + '.'
);
break;
}
}
if (behaviorSpecificProps.sn === this._state.toString()) {
this._state.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
// And we are not using the default controls.
this._ignoreDefaultControlsAsSyncedByNetwork = true;
this._ignoreDefaultControlsAsSyncedByNetwork = !options.keepControl;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -545,8 +559,9 @@ namespace gdjs {
this._wasJumpKeyPressed = this._jumpKey;
this._wasReleasePlatformKeyPressed = this._releasePlatformKey;
this._wasReleaseLadderKeyPressed = this._releaseLadderKey;
//4) Do not forget to reset pressed keys
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
// Reset the keys only if the inputs are not supposed to survive between frames.
// (Most of the time, except if this object is synchronized by an external source)
this._leftKey = false;

View File

@@ -157,6 +157,57 @@ namespace gdjs {
return true;
}
trackChangesAndUpdateManagerIfNeeded() {
if (!this.activated() && this._registeredInManager) {
this._manager.removePlatform(this);
this._registeredInManager = false;
} else {
if (this.activated() && !this._registeredInManager) {
this._manager.addPlatform(this);
this._registeredInManager = true;
}
}
if (
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
this._manager.addPlatform(this);
}
this._oldX = this.owner.getX();
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BehaviorNetworkSyncData {
return super.getNetworkSyncData(syncOptions);
}
updateFromNetworkSyncData(
networkSyncData: BehaviorNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
this.trackChangesAndUpdateManagerIfNeeded();
}
onCreated(): void {
// Register it right away if activated,
// so it can be used by platformer objects in that same frame.
this.trackChangesAndUpdateManagerIfNeeded();
}
onDestroy() {
if (this._manager && this._registeredInManager) {
this._manager.removePlatform(this);
@@ -175,34 +226,7 @@ namespace gdjs {
}*/
//Make sure the platform is or is not in the platforms manager.
if (!this.activated() && this._registeredInManager) {
this._manager.removePlatform(this);
this._registeredInManager = false;
} else {
if (this.activated() && !this._registeredInManager) {
this._manager.addPlatform(this);
this._registeredInManager = true;
}
}
//Track changes in size or position
if (
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
this._manager.addPlatform(this);
}
this._oldX = this.owner.getX();
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
this.trackChangesAndUpdateManagerIfNeeded();
}
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}

View File

@@ -37,6 +37,24 @@ namespace gdjs {
export type ShapePainterObjectData = ObjectData & ShapePainterObjectDataType;
type ShapePainterNetworkSyncDataType = {
cbf: boolean; // clearBetweenFrames
aa: Antialiasing; // antialiasing
ac: boolean; // absoluteCoordinates
fc: integer; // fillColor
oc: integer; // outlineColor
os: float; // outlineSize
fo: float; // fillOpacity
oo: float; // outlineOpacity
scaleX: number;
scaleY: number;
ifx: boolean; // isFlippedX
ify: boolean; // isFlippedY
};
export type ShapePainterNetworkSyncData = ObjectNetworkSyncData &
ShapePainterNetworkSyncDataType;
/**
* The ShapePainterRuntimeObject allows to draw graphics shapes on screen.
*/
@@ -191,6 +209,70 @@ namespace gdjs {
return true;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): ShapePainterNetworkSyncData {
return {
...super.getNetworkSyncData(syncOptions),
cbf: this._clearBetweenFrames,
aa: this._antialiasing,
ac: this._useAbsoluteCoordinates,
fc: this._fillColor,
oc: this._outlineColor,
os: this._outlineSize,
fo: this._fillOpacity,
oo: this._outlineOpacity,
scaleX: this.getScaleX(),
scaleY: this.getScaleY(),
ifx: this._flippedX,
ify: this._flippedY,
};
}
updateFromNetworkSyncData(
networkSyncData: ShapePainterNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.cbf !== undefined) {
this._clearBetweenFrames = networkSyncData.cbf;
}
if (networkSyncData.aa !== undefined) {
this.setAntialiasing(networkSyncData.aa);
}
if (networkSyncData.ac !== undefined) {
this.setCoordinatesRelative(!networkSyncData.ac);
}
if (networkSyncData.fc !== undefined) {
this._fillColor = networkSyncData.fc;
}
if (networkSyncData.oc !== undefined) {
this._outlineColor = networkSyncData.oc;
}
if (networkSyncData.os !== undefined) {
this.setOutlineSize(networkSyncData.os);
}
if (networkSyncData.fo !== undefined) {
this.setFillOpacity(networkSyncData.fo);
}
if (networkSyncData.oo !== undefined) {
this.setOutlineOpacity(networkSyncData.oo);
}
if (networkSyncData.scaleX !== undefined) {
this.setScaleX(networkSyncData.scaleX);
}
if (networkSyncData.scaleY !== undefined) {
this.setScaleY(networkSyncData.scaleY);
}
if (networkSyncData.ifx !== undefined) {
this.flipX(networkSyncData.ifx);
}
if (networkSyncData.ify !== undefined) {
this.flipY(networkSyncData.ify);
}
}
/**
* Initialize the extra parameters that could be set for an instance.
* @param initialInstanceData The extra parameters

View File

@@ -0,0 +1,488 @@
//@ts-check
/// <reference path="../JsExtensionTypes.d.ts" />
/**
* This is a declaration of an extension for GDevelop 5.
*
* Changes in this file are watched and automatically imported if the editor
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/** @type {ExtensionModule} */
module.exports = {
createExtension: function (_, gd) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'SaveState',
_('Save State (experimental)'),
_(
'Allows to save and load the full state of a game, usually on the device storage. A Save State, by default, contains the full state of the game (objects, variables, sounds, music, effects etc.). Using the "Save Configuration" behavior, you can customize which objects should not be saved in a Save State. You can also use the "Change the save configuration of a variable" action to change the save configuration of a variable. Finally, both objects, variables and scene/game data can be given a profile name: in this case, when saving or loading with one or more profile names specified, only the object/variables/data belonging to one of the specified profiles will be saved or loaded.'
),
'Neyl Mahfouf',
'Open source (MIT License)'
)
.setExtensionHelpPath('/all-features/save-state')
.setCategory('Game mechanic')
.addInstructionOrExpressionGroupMetadata(_('Save State (experimental)'))
.setIcon('res/actions/saveDown.svg');
extension
.addAction(
'CreateGameSaveStateInVariable',
_('Save game to a variable'),
_(
'Create a Save State and save it to a variable. This is for advanced usage, prefer to use "Save game to device storage" in most cases.'
),
_('Save game in variable _PARAM1_ (profile(s): _PARAM2_)'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable to store the save to'), '', false)
.addParameter('string', _('Profile(s) to save'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.createGameSaveStateInVariable');
extension
.addAction(
'CreateGameSaveStateInStorage',
_('Save game to device storage'),
_('Create a Save State and save it to device storage.'),
_('Save game to device storage named _PARAM1_ (profile(s): _PARAM2_)'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('string', _('Storage key to save to'), '', false)
.addParameter('string', _('Profile(s) to save'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.createGameSaveStateInStorage');
extension
.addAction(
'RestoreGameSaveStateFromVariable',
_('Load game from variable'),
_(
'Restore the game from a Save State stored in the specified variable. This is for advanced usage, prefer to use "Load game from device storage" in most cases.'
),
_(
'Load game from variable _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable to load the game from'), '', false)
.addParameter('string', _('Profile(s) to load'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.addParameter(
'yesorno',
_('Stop and restart all the scenes currently played?'),
'',
true
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromVariable');
extension
.addAction(
'RestoreGameSaveStateFromStorage',
_('Load game from device storage'),
_('Restore the game from a Save State stored on the device.'),
_(
'Load game from device storage named _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'string',
_('Storage name to load the game from'),
'',
false
)
.addParameter('string', _('Profile(s) to load'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded.'
)
)
.addParameter(
'yesorno',
_('Stop and restart all the scenes currently played?'),
'',
true
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromStorage');
extension
.addExpressionAndCondition(
'number',
'TimeSinceLastSave',
_('Time since last save'),
_(
'Time since the last save, in seconds. Returns -1 if no save happened, and a positive number otherwise.'
),
_('Time since the last save'),
'',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.getSecondsSinceLastSave')
.setGetter('gdjs.saveState.getSecondsSinceLastSave');
extension
.addExpressionAndCondition(
'number',
'TimeSinceLastLoad',
_('Time since last load'),
_(
'Time since the last load, in seconds. Returns -1 if no load happened, and a positive number otherwise.'
),
_('Time since the last load'),
'',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.getSecondsSinceLastLoad')
.setGetter('gdjs.saveState.getSecondsSinceLastLoad');
extension
.addCondition(
'SaveJustSucceeded',
_('Save just succeeded'),
_('The last save attempt just succeeded.'),
_('Save just succeeded'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.hasSaveJustSucceeded');
extension
.addCondition(
'SaveJustFailed',
_('Save just failed'),
_('The last save attempt just failed.'),
_('Save just failed'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.hasSaveJustFailed');
extension
.addCondition(
'LoadJustSucceeded',
_('Load just succeeded'),
_('The last load attempt just succeeded.'),
_('Load just succeeded'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.hasLoadJustSucceeded');
extension
.addCondition(
'LoadJustFailed',
_('Load just failed'),
_('The last load attempt just failed.'),
_('Load just failed'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.hasLoadJustFailed');
extension
.addAction(
'SetVariableSaveConfiguration',
_('Change the save configuration of a variable'),
_(
'Set if a scene or global variable should be saved in the default save state. Also allow to specify one or more profiles in which the variable should be saved.'
),
_(
'Change save configuration of _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'variable',
_('Variable for which configuration should be changed'),
'',
false
)
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the variable should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the variable will be saved. When a save state is created with one or more profile names specified, the variable will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setVariableSaveConfiguration');
extension
.addAction(
'SetGameDataSaveConfiguration',
_('Change the save configuration of the global game data'),
_(
'Set if the global game data (audio & global variables) should be saved in the default save state. Also allow to specify one or more profiles in which the global game data should be saved.'
),
_(
'Change save configuration of global game data: save them in the default save states: _PARAM1_ and in profiles: _PARAM2_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the global game data should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the global game data will be saved. When a save state is created with one or more profile names specified, the global game data will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setGameDataSaveConfiguration');
extension
.addAction(
'SetSceneDataSaveConfiguration',
_('Change the save configuration of a scene data'),
_(
'Set if the data of the specified scene (scene variables, timers, trigger once, wait actions, layers, etc.) should be saved in the default save state. Also allow to specify one or more profiles in which the scene data should be saved. Note: objects are always saved separately from the scene data (use the "Save Configuration" behavior to customize the configuration of objects).'
),
_(
'Change save configuration of scene _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'sceneName',
_('Scene name for which configuration should be changed'),
'',
false
)
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the scene data should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the scene data will be saved. When a save state is created with one or more profile names specified, the scene data will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setSceneDataSaveConfiguration');
// Save Configuration behavior
const saveConfigurationBehavior = new gd.BehaviorJsImplementation();
saveConfigurationBehavior.updateProperty = function (
behaviorContent,
propertyName,
newValue
) {
if (propertyName === 'defaultProfilePersistence') {
behaviorContent
.getChild('defaultProfilePersistence')
.setStringValue(newValue);
return true;
}
if (propertyName === 'persistedInProfiles') {
behaviorContent
.getChild('persistedInProfiles')
.setStringValue(newValue);
return true;
}
return false;
};
saveConfigurationBehavior.getProperties = function (behaviorContent) {
const behaviorProperties = new gd.MapStringPropertyDescriptor();
behaviorProperties
.getOrCreate('defaultProfilePersistence')
.setValue(
behaviorContent.getChild('defaultProfilePersistence').getStringValue()
)
.setType('Choice')
.setLabel(_('Persistence mode'))
.addChoice('Persisted', _('Include in save states (default)'))
.addChoice('DoNotSave', _('Do not save'));
behaviorProperties
.getOrCreate('persistedInProfiles')
.setValue(
behaviorContent.getChild('persistedInProfiles').getStringValue()
)
.setType('String')
.setLabel(_('Save profile names'))
.setDescription(
_(
'Comma-separated list of profile names in which the object is saved. When a save state is created with one or more profile names specified, the object will be saved only if it matches one of these profiles.'
)
)
.setAdvanced(true);
return behaviorProperties;
};
saveConfigurationBehavior.initializeContent = function (behaviorContent) {
behaviorContent
.addChild('defaultProfilePersistence')
.setStringValue('Persisted');
behaviorContent.addChild('persistedInProfiles').setStringValue('');
};
const sharedData = new gd.BehaviorsSharedData();
extension
.addBehavior(
'SaveConfiguration',
_('Save state configuration'),
'SaveConfiguration',
_('Allow the customize how the object is persisted in a save state.'),
'',
'res/actions/saveUp.svg',
'SaveConfiguration',
// @ts-ignore - TODO: Fix type being a BehaviorJsImplementation instead of an Behavior
saveConfigurationBehavior,
sharedData
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
);
return extension;
},
runExtensionSanityTests: function (gd, extension) {
return [];
},
};

View File

@@ -0,0 +1,32 @@
namespace gdjs {
// const logger = new gdjs.Logger('Save State');
export class SaveConfigurationRuntimeBehavior extends gdjs.RuntimeBehavior {
private readonly _defaultProfilePersistence: 'Persisted' | 'DoNotSave' =
'Persisted';
private readonly _persistedInProfiles = '';
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
behaviorData: any,
owner: RuntimeObject
) {
super(instanceContainer, behaviorData, owner);
this._defaultProfilePersistence =
behaviorData.defaultProfilePersistence || 'Persisted';
this._persistedInProfiles = behaviorData.persistedInProfiles || '';
}
getDefaultProfilePersistence() {
return this._defaultProfilePersistence;
}
getPersistedInProfiles() {
return this._persistedInProfiles;
}
}
gdjs.registerBehavior(
'SaveState::SaveConfiguration',
gdjs.SaveConfigurationRuntimeBehavior
);
}

View File

@@ -0,0 +1,696 @@
namespace gdjs {
const logger = new gdjs.Logger('Save State');
const debugLogger = new gdjs.Logger('Save State - Debug');
// Comment this to see message logs and ease debugging:
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(
'Save State - Debug'
);
type ArbitrarySaveConfiguration = {
defaultProfilePersistence: 'Persisted' | 'DoNotSave';
persistedInProfiles: Set<string>;
};
export type RestoreRequestOptions = {
profileNames: string[];
clearSceneStack: boolean;
fromStorageName?: string;
fromVariable?: gdjs.Variable;
};
export namespace saveState {
export const getIndexedDbDatabaseName = () => {
const gameId = gdjs.projectData.properties.projectUuid;
return `gdevelop-game-${gameId}`;
};
export const getIndexedDbObjectStore = () => {
return `game-saves`;
};
export const getIndexedDbStorageKey = (key: string) => {
return `save-${key}`;
};
const variablesSaveConfiguration: WeakMap<
Variable,
ArbitrarySaveConfiguration
> = new WeakMap();
const runtimeSceneDataSaveConfiguration: WeakMap<
RuntimeGame,
Record<string, ArbitrarySaveConfiguration>
> = new WeakMap();
const runtimeGameDataSaveConfiguration: WeakMap<
RuntimeGame,
ArbitrarySaveConfiguration
> = new WeakMap();
export const setVariableSaveConfiguration = (
_: gdjs.RuntimeScene,
variable: gdjs.Variable,
persistInDefaultProfile: boolean,
persistedInProfilesAsString: string
) => {
variablesSaveConfiguration.set(variable, {
defaultProfilePersistence: persistInDefaultProfile
? 'Persisted'
: 'DoNotSave',
persistedInProfiles: new Set(
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
),
});
};
export const setSceneDataSaveConfiguration = (
runtimeScene: gdjs.RuntimeScene,
sceneName: string,
persistInDefaultProfile: boolean,
persistedInProfilesAsString: string
) => {
const runtimeSceneDataSaveConfigurations =
runtimeSceneDataSaveConfiguration.get(runtimeScene.getGame()) || {};
runtimeSceneDataSaveConfiguration.set(runtimeScene.getGame(), {
...runtimeSceneDataSaveConfigurations,
[sceneName]: {
defaultProfilePersistence: persistInDefaultProfile
? 'Persisted'
: 'DoNotSave',
persistedInProfiles: new Set(
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
),
},
});
};
export const setGameDataSaveConfiguration = (
runtimeScene: gdjs.RuntimeScene,
persistInDefaultProfile: boolean,
persistedInProfilesAsString: string
) => {
runtimeGameDataSaveConfiguration.set(runtimeScene.getGame(), {
defaultProfilePersistence: persistInDefaultProfile
? 'Persisted'
: 'DoNotSave',
persistedInProfiles: new Set(
parseCommaSeparatedProfileNames(persistedInProfilesAsString)
),
});
};
const checkIfIsPersistedInProfiles = (
profileNames: string[],
configuration: ArbitrarySaveConfiguration | null | undefined
) => {
if (profileNames.includes('default')) {
if (
!configuration ||
configuration.defaultProfilePersistence === 'Persisted'
) {
return true;
}
}
if (configuration) {
for (const profileName of profileNames) {
if (configuration.persistedInProfiles.has(profileName)) {
return true;
}
}
}
return false;
};
const makeIsVariableExcludedFromSaveState =
(profileNames: string[]) => (variable: gdjs.Variable) => {
const saveConfiguration = variablesSaveConfiguration.get(variable);
return !checkIfIsPersistedInProfiles(
profileNames,
saveConfiguration || null
);
};
let lastSaveTime: number | null = null;
let lastLoadTime: number | null = null;
let saveJustSucceeded: boolean = false;
let saveJustFailed: boolean = false;
let loadJustSucceeded: boolean = false;
let loadJustFailed: boolean = false;
let restoreRequestOptions: RestoreRequestOptions | null = null;
export const getSecondsSinceLastSave = (_: RuntimeScene): number => {
if (!lastSaveTime) return -1;
return Math.floor((Date.now() - lastSaveTime) / 1000);
};
export const getSecondsSinceLastLoad = (_: RuntimeScene): number => {
if (!lastLoadTime) return -1;
return Math.floor((Date.now() - lastLoadTime) / 1000);
};
export const hasSaveJustSucceeded = (_: RuntimeScene) => {
return saveJustSucceeded;
};
export const hasLoadJustSucceeded = (_: RuntimeScene) => {
return loadJustSucceeded;
};
export const hasSaveJustFailed = (_: RuntimeScene) => {
return saveJustFailed;
};
export const hasLoadJustFailed = (_: RuntimeScene) => {
return loadJustFailed;
};
export const markSaveJustSucceeded = (_: RuntimeScene) => {
saveJustSucceeded = true;
lastSaveTime = Date.now();
};
export const markLoadJustSucceeded = (_: RuntimeScene) => {
loadJustSucceeded = true;
lastLoadTime = Date.now();
};
export const markSaveJustFailed = (_: RuntimeScene) => {
saveJustFailed = true;
};
export const markLoadJustFailed = (_: RuntimeScene) => {
loadJustFailed = true;
};
// Ensure that the condition "save/load just succeeded/failed" are valid only for one frame.
gdjs.registerRuntimeScenePostEventsCallback(() => {
saveJustSucceeded = false;
saveJustFailed = false;
loadJustSucceeded = false;
loadJustFailed = false;
});
gdjs.registerRuntimeScenePostEventsCallback(
(runtimeScene: gdjs.RuntimeScene) => {
checkAndRestoreGameSaveStateAtEndOfFrame(runtimeScene);
}
);
/**
* Create a Save State from the given game.
*
* Only objects, variables etc... tagged with at least one of the profiles
* given in `options.profileNames` will be saved.
*/
export const createGameSaveState = (
runtimeGame: RuntimeGame,
options: {
profileNames: string[];
}
) => {
const { profileNames } = options;
const getNetworkSyncOptions: GetNetworkSyncDataOptions = {
syncObjectIdentifiers: true,
shouldExcludeVariableFromData:
makeIsVariableExcludedFromSaveState(profileNames),
syncAllBehaviors: true,
syncGameVariables: true,
syncSceneTimers: true,
syncOnceTriggers: true,
syncSounds: true,
syncTweens: true,
syncLayers: true,
syncAsyncTasks: true,
syncSceneVisualProps: true,
syncFullTileMaps: true,
};
const shouldPersistGameData = checkIfIsPersistedInProfiles(
options.profileNames,
runtimeGameDataSaveConfiguration.get(runtimeGame)
);
const gameSaveState: GameSaveState = {
// Always persist some game data, but limit it to just the scene stack
// if asked to not persist the game data.
gameNetworkSyncData: runtimeGame.getNetworkSyncData({
...getNetworkSyncOptions,
syncGameVariables: shouldPersistGameData,
syncSounds: shouldPersistGameData,
}),
layoutNetworkSyncDatas: [],
};
const scenes = runtimeGame.getSceneStack().getAllScenes();
scenes.forEach((runtimeScene, index) => {
gameSaveState.layoutNetworkSyncDatas[index] = {
sceneData: {} as LayoutNetworkSyncData,
objectDatas: {},
};
// First collect all object sync data, as they may generate unique
// identifiers like their networkId.
for (const object of runtimeScene.getAdhocListOfAllInstances()) {
// By default, an object which has no SaveConfiguration behavior is like
// it has the default profile persistence set to "Persisted".
let shouldPersist = profileNames.includes('default');
// @ts-ignore - access to `_behaviors` is an exceptional case for the SaveConfiguration behavior.
for (const behavior of object._behaviors) {
if (behavior instanceof gdjs.SaveConfigurationRuntimeBehavior) {
// This object has a SaveConfiguration behavior. Check if the configuration is set to
// persist it in one of the given profiles.
if (
(profileNames.includes('default') &&
behavior.getDefaultProfilePersistence() === 'Persisted') ||
profileNames.some((profileName) =>
// TODO: avoid do it for every single object instance?
behavior
.getPersistedInProfiles()
.split(',')
.map((profileName) => profileName.trim())
.includes(profileName)
)
) {
shouldPersist = true;
} else {
shouldPersist = false;
}
}
}
if (shouldPersist) {
const objectSyncData = object.getNetworkSyncData(
getNetworkSyncOptions
);
gameSaveState.layoutNetworkSyncDatas[index].objectDatas[object.id] =
objectSyncData;
}
}
// Collect scene data after the objects:
const shouldPersistSceneData = checkIfIsPersistedInProfiles(
options.profileNames,
(runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[
runtimeScene.getName()
]
);
if (shouldPersistSceneData) {
const sceneData = runtimeScene.getNetworkSyncData(
getNetworkSyncOptions
);
if (sceneData) {
gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneData;
}
}
});
return gameSaveState;
};
export const createGameSaveStateInVariable = async function (
runtimeScene: RuntimeScene,
variable: gdjs.Variable,
commaSeparatedProfileNames: string
) {
try {
const gameSaveState = createGameSaveState(runtimeScene.getGame(), {
profileNames: parseCommaSeparatedProfileNamesOrDefault(
commaSeparatedProfileNames
),
});
variable.fromJSObject(gameSaveState);
markSaveJustSucceeded(runtimeScene);
} catch (error) {
logger.error('Error saving to variable:', error);
markSaveJustFailed(runtimeScene);
}
};
export const createGameSaveStateInStorage = async function (
runtimeScene: RuntimeScene,
storageKey: string,
commaSeparatedProfileNames: string
) {
try {
const gameSaveState = createGameSaveState(runtimeScene.getGame(), {
profileNames: parseCommaSeparatedProfileNamesOrDefault(
commaSeparatedProfileNames
),
});
await gdjs.indexedDb.saveToIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(storageKey),
gameSaveState
);
markSaveJustSucceeded(runtimeScene);
} catch (error) {
logger.error('Error saving to IndexedDB:', error);
markSaveJustFailed(runtimeScene);
}
};
const checkAndRestoreGameSaveStateAtEndOfFrame = function (
runtimeScene: RuntimeScene
) {
const runtimeGame = runtimeScene.getGame();
if (!restoreRequestOptions) return;
const { fromVariable, fromStorageName, profileNames, clearSceneStack } =
restoreRequestOptions;
// Reset it so we don't load it twice.
restoreRequestOptions = null;
if (fromVariable) {
const saveState = fromVariable.toJSObject();
try {
restoreGameSaveState(runtimeGame, saveState, {
profileNames,
clearSceneStack,
});
markLoadJustSucceeded(runtimeScene);
} catch (error) {
logger.error('Error loading from variable:', error);
markLoadJustFailed(runtimeScene);
}
} else if (fromStorageName) {
gdjs.indexedDb
.loadFromIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(fromStorageName)
)
.then((jsonData) => {
const saveState = jsonData as GameSaveState;
restoreGameSaveState(runtimeGame, saveState, {
profileNames,
clearSceneStack,
});
markLoadJustSucceeded(runtimeScene);
})
.catch((error) => {
logger.error('Error loading from IndexedDB:', error);
markLoadJustFailed(runtimeScene);
});
}
};
const getInstanceFromNetworkId = ({
runtimeScene,
objectName,
networkId,
}: {
runtimeScene: gdjs.RuntimeScene;
objectName: string;
networkId: string;
}): gdjs.RuntimeObject | null => {
const instances = runtimeScene.getInstancesOf(objectName);
if (!instances) {
// object does not exist in the scene, cannot find the instance.
return null;
}
let instance =
instances.find((instance) => instance.networkId === networkId) || null;
// Check if there is already an instance with the given network ID.
if (instance) {
debugLogger.info(
`Found instance ${networkId}, will use it for restoring.`
);
return instance;
}
// Instance not found - it must have been deleted. Create it now.
debugLogger.info(
`Instance ${networkId} not found, creating instance ${objectName}.`
);
const newInstance = runtimeScene.createObject(objectName);
if (!newInstance) {
// Object does not exist in the scene, cannot create the instance.
return null;
}
newInstance.networkId = networkId;
return newInstance;
};
/**
* Restore the game using the given Save State.
*
* `options.profileNames` is the list of profiles to restore: only objects, variables etc... tagged with at least
* one of these profiles will be restored (or recreated if they don't exist, or deleted if not in the save state).
* Others will be left untouched.
*
* If `options.clearSceneStack` is true, all the scenes will be unloaded and re-created
* (meaning all instances will be re-created, variables will go back to their initial values, etc...).
* Otherwise, the existing scenes will be updated (or unloaded or created if the save state has different scenes).
*/
export const restoreGameSaveState = (
runtimeGame: RuntimeGame,
saveState: GameSaveState,
options: {
profileNames: string[];
clearSceneStack: boolean;
}
): void => {
const getObjectNamesToRestoreForRuntimeScene = (
runtimeScene: RuntimeScene
): Set<string> => {
const allObjectData = [];
runtimeScene._objects.values(allObjectData);
return getObjectNamesIncludedInProfiles(
allObjectData,
options.profileNames
);
};
const updateFromNetworkSyncDataOptions: UpdateFromNetworkSyncDataOptions =
{
clearSceneStack:
options.clearSceneStack === undefined
? true
: options.clearSceneStack,
getExcludedObjectNames: getObjectNamesToRestoreForRuntimeScene,
preventSoundsStoppingOnStartup: true,
clearInputs: true,
keepControl: true,
ignoreVariableOwnership: true,
shouldExcludeVariableFromUpdate: makeIsVariableExcludedFromSaveState(
options.profileNames
),
};
// First update the game, which will update the variables,
// and set the scene stack to update when ready.
if (saveState.gameNetworkSyncData) {
const shouldRestoreGameData = checkIfIsPersistedInProfiles(
options.profileNames,
runtimeGameDataSaveConfiguration.get(runtimeGame)
);
runtimeGame.updateFromNetworkSyncData(
shouldRestoreGameData
? saveState.gameNetworkSyncData
: {
// Disable game data restoration if asked to, but
// still always keep `ss` (scene stack) restoration as it's always needed.
ss: saveState.gameNetworkSyncData.ss,
},
updateFromNetworkSyncDataOptions
);
}
// Apply the scene stack updates, as we are at the end of a frame,
// we can safely do it.
const sceneStack = runtimeGame.getSceneStack();
sceneStack.applyUpdateFromNetworkSyncDataIfAny(
updateFromNetworkSyncDataOptions
);
// Then get all scenes, which we assume will be the expected ones
// after the load has been done, so we can update them,
// and create their objects.
const runtimeScenes = sceneStack.getAllScenes();
runtimeScenes.forEach((runtimeScene, index) => {
const layoutSyncData = saveState.layoutNetworkSyncDatas[index];
if (!layoutSyncData) return;
// List names of objects that must be restored
// (and only them - instances of others will be left alone).
const objectNamesToRestore =
getObjectNamesToRestoreForRuntimeScene(runtimeScene);
// Create objects first, so they are available for the scene update,
// especially so that they have a networkId defined.
const allLoadedNetworkIds = new Set<string>();
const objectDatas = layoutSyncData.objectDatas;
for (const id in objectDatas) {
const objectNetworkSyncData = objectDatas[id];
const objectName = objectNetworkSyncData.n;
if (!objectName) {
logger.warn('Tried to recreate an object without a name.');
continue;
}
if (!objectNamesToRestore.has(objectName)) {
// Object is in the save state, but not in the profiles to restore, don't restore it.
continue;
}
// Object is both in the save state and in the profiles to restore, restore it.
// Either find the existing instance with the same networkId, or create a new one.
const networkId = objectNetworkSyncData.networkId || '';
allLoadedNetworkIds.add(networkId);
const object = getInstanceFromNetworkId({
runtimeScene,
objectName: objectName,
networkId,
});
if (object) {
object.updateFromNetworkSyncData(
objectNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
// Clean instances of objects that are not in the profiles to restore but not in the save state
// (i.e: those who don't have a networkId, or it's not in the save state: they must not exist).
for (const objectName of objectNamesToRestore) {
// /!\ Clone the instances to avoid it being modified while iterating through them.
const objects = [...runtimeScene.getInstancesOf(objectName)];
for (const object of objects) {
// This is an object instance that is part of the object that are being restored,
// but it has not network id (created after the save state was created) or the network
// id is not in the save state: it's not part of the save state and must be deleted.
if (
!object.networkId ||
!allLoadedNetworkIds.has(object.networkId)
) {
object.deleteFromScene();
}
}
}
// Update the rest of the scene last.
if (
checkIfIsPersistedInProfiles(
options.profileNames,
(runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[
runtimeScene.getName()
]
)
) {
runtimeScene.updateFromNetworkSyncData(
layoutSyncData.sceneData,
updateFromNetworkSyncDataOptions
);
}
});
};
const parseCommaSeparatedProfileNames = (
commaSeparatedProfileNames: string
): string[] | null => {
if (!commaSeparatedProfileNames) return null;
return commaSeparatedProfileNames
.split(',')
.map((profileName) => profileName.trim());
};
const parseCommaSeparatedProfileNamesOrDefault = (
commaSeparatedProfileNames: string
): string[] => {
return (
parseCommaSeparatedProfileNames(commaSeparatedProfileNames) || [
'default',
]
);
};
export const restoreGameSaveStateFromVariable = async function (
_: gdjs.RuntimeScene,
variable: gdjs.Variable,
commaSeparatedProfileNames: string,
clearSceneStack: boolean
) {
// The information is saved, so that the restore can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
restoreRequestOptions = {
fromVariable: variable,
profileNames: parseCommaSeparatedProfileNamesOrDefault(
commaSeparatedProfileNames
),
clearSceneStack,
};
};
export const restoreGameSaveStateFromStorage = async function (
_: gdjs.RuntimeScene,
storageName: string,
commaSeparatedProfileNames: string,
clearSceneStack: boolean
) {
// The information is saved, so that the restore can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
restoreRequestOptions = {
fromStorageName: storageName,
profileNames: parseCommaSeparatedProfileNamesOrDefault(
commaSeparatedProfileNames
),
clearSceneStack,
};
};
/**
* Compute, by looking at the "static" object data (i.e: in the Project Data),
* the name of objects which must be restored, based on the given profiles.
*/
const getObjectNamesIncludedInProfiles = (
allObjectData: ObjectData[],
profileNames: string[]
): Set<string> => {
const objectNames = new Set<string>();
for (const objectData of allObjectData) {
// By default, an object which has no SaveConfiguration behavior is like
// it has the default profile persistence set to "Persisted".
let includedInProfiles = profileNames.includes('default');
for (const behaviorData of objectData.behaviors) {
if (behaviorData.type !== 'SaveState::SaveConfiguration') continue;
const defaultProfilePersistence =
behaviorData.defaultProfilePersistence === 'Persisted'
? 'Persisted'
: 'DoNotSave';
const persistedInProfiles =
typeof behaviorData.persistedInProfiles === 'string'
? behaviorData.persistedInProfiles
.split(',')
.map((profileName: string) => profileName.trim())
: [];
// This object has a SaveConfiguration behavior. Check if the configuration is set to
// persist it in one of the given profiles.
includedInProfiles = false;
if (
(profileNames.includes('default') &&
defaultProfilePersistence === 'Persisted') ||
profileNames.some((profileName) =>
persistedInProfiles.includes(profileName)
)
) {
// This object must be persisted in one of the given profile.
includedInProfiles = true;
}
}
if (includedInProfiles) {
objectNames.add(objectData.name);
}
}
return objectNames;
};
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -53,6 +53,8 @@ namespace gdjs {
readonly spineResourceName: string;
static isHitBoxesUpdateDisabled = false;
/**
* @param instanceContainer The container the object belongs to.
* @param objectData The object data used to initialize the object
@@ -74,6 +76,10 @@ namespace gdjs {
this.setAnimationIndex(0);
this._renderer.updateAnimation(0);
if (SpineRuntimeObject.isHitBoxesUpdateDisabled) {
this.hitBoxes.length = 0;
}
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
@@ -111,9 +117,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SpineNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SpineNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
opa: this._opacity,
scaX: this.getScaleX(),
scaY: this.getScaleY(),
@@ -127,8 +135,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: SpineNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: SpineNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (syncData.opa !== undefined && syncData.opa !== this._opacity) {
this.setOpacity(syncData.opa);
@@ -183,6 +194,14 @@ namespace gdjs {
}
}
updateHitBoxes(): void {
if (SpineRuntimeObject.isHitBoxesUpdateDisabled) {
return;
}
super.updateHitBoxes();
}
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {

View File

@@ -267,9 +267,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TextInputNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TextInputNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
opa: this.getOpacity(),
txt: this.getText(),
frn: this.getFontResourceName(),
@@ -288,8 +290,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: TextInputNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: TextInputNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (syncData.opa !== undefined) this.setOpacity(syncData.opa);
if (syncData.txt !== undefined) this.setText(syncData.txt);

View File

@@ -52,6 +52,7 @@ namespace gdjs {
c: number[];
scale: number;
ta: string;
vta: string;
wrap: boolean;
wrapw: float;
oena: boolean;
@@ -179,9 +180,6 @@ namespace gdjs {
if (oldContent.text !== newContent.text) {
this.setText(newContent.text);
}
if (oldContent.underlined !== newContent.underlined) {
return false;
}
if (oldContent.textAlignment !== newContent.textAlignment) {
this.setTextAlignment(newContent.textAlignment);
}
@@ -220,12 +218,18 @@ namespace gdjs {
if ((oldContent.lineHeight || 0) !== (newContent.lineHeight || 0)) {
this.setLineHeight(newContent.lineHeight || 0);
}
if (oldContent.underlined !== newContent.underlined) {
return false;
}
return true;
}
override getNetworkSyncData(): TextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
str: this._str,
o: this.opacity,
cs: this._characterSize,
@@ -236,6 +240,7 @@ namespace gdjs {
c: this._color,
scale: this.getScale(),
ta: this._textAlign,
vta: this._verticalTextAlignment,
wrap: this._wrapping,
wrapw: this._wrappingWidth,
oena: this._isOutlineEnabled,
@@ -253,9 +258,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: TextObjectNetworkSyncData
networkSyncData: TextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.str !== undefined) {
this.setText(networkSyncData.str);
}
@@ -286,8 +292,8 @@ namespace gdjs {
if (networkSyncData.ta !== undefined) {
this.setTextAlignment(networkSyncData.ta);
}
if (networkSyncData.ta !== undefined) {
this.setVerticalTextAlignment(networkSyncData.ta);
if (networkSyncData.vta !== undefined) {
this.setVerticalTextAlignment(networkSyncData.vta);
}
if (networkSyncData.wrap !== undefined) {
this.setWrapping(networkSyncData.wrap);

View File

@@ -9,7 +9,7 @@ import {
* A tile map model.
*
* Tile map files are parsed into this model by {@link TiledTileMapLoader} or {@link LDtkTileMapLoader}.
* This model is used for rending ({@link TileMapRuntimeObjectPixiRenderer})
* This model is used for rendering ({@link TileMapRuntimeObjectPixiRenderer})
* and hitboxes handling ({@link TransformedCollisionTileMap}).
* This allows to support new file format with only a new parser.
*/
@@ -71,7 +71,7 @@ export declare class EditableTileMap {
tileSetRowCount: number;
}
): EditableTileMap;
toJSObject(): Object;
toJSObject(): EditableTileMapAsJsObject;
/**
* @returns The tile map width in pixels.
*/
@@ -205,7 +205,7 @@ declare abstract class AbstractEditableLayer {
*/
constructor(tileMap: EditableTileMap, id: integer);
setVisible(visible: boolean): void;
toJSObject(): Object;
toJSObject(): EditableTileMapLayerAsJsObject;
/**
* @returns true if the layer is visible.
*/
@@ -284,7 +284,7 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer;
toJSObject(): Object;
toJSObject(): EditableTileMapLayerAsJsObject;
/**
* The opacity (between 0-1) of the layer
*/

View File

@@ -2,7 +2,6 @@
namespace gdjs {
export type SimpleTileMapObjectDataType = {
content: {
opacity: number;
atlasImage: string;
rowCount: number;
columnCount: number;
@@ -16,8 +15,7 @@ namespace gdjs {
export type SimpleTileMapNetworkSyncDataType = {
op: number;
ai: string;
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
tm?: TileMapHelper.EditableTileMapAsJsObject;
};
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
@@ -38,6 +36,7 @@ namespace gdjs {
_opacity: float = 255;
_atlasImage: string;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_tileMap: TileMapHelper.EditableTileMap | null = null;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
readonly _rowCount: number;
readonly _columnCount: number;
@@ -62,7 +61,6 @@ namespace gdjs {
objectData: SimpleTileMapObjectDataType
) {
super(instanceContainer, objectData);
this._opacity = objectData.content.opacity;
this._atlasImage = objectData.content.atlasImage;
this._rowCount = objectData.content.rowCount;
this._columnCount = objectData.content.columnCount;
@@ -87,16 +85,19 @@ namespace gdjs {
instanceContainer
);
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
this._renderer.updatePosition();
this._loadTileMap(
this._initialTileMapAsJsObject,
(tileMap: TileMapHelper.EditableTileMap) => {
this._renderer.updatePosition();
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
this.updateTransformation();
});
this.updateTransformation();
}
);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
@@ -139,8 +140,8 @@ namespace gdjs {
);
if (!shouldContinue) return;
if (this._collisionTileMap) {
const tileMap = this._renderer.getTileMap();
if (tileMap) this._collisionTileMap.updateFromTileMap(tileMap);
if (this._tileMap)
this._collisionTileMap.updateFromTileMap(this._tileMap);
}
this._isTileMapDirty = false;
}
@@ -150,9 +151,6 @@ namespace gdjs {
oldObjectData: SimpleTileMapObjectData,
newObjectData: SimpleTileMapObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
) {
@@ -163,27 +161,58 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SimpleTileMapNetworkSyncData {
return {
...super.getNetworkSyncData(),
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SimpleTileMapNetworkSyncData {
const syncData: SimpleTileMapNetworkSyncData = {
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
ai: this._atlasImage,
};
if (this._tileMap && syncOptions.syncFullTileMaps) {
const currentTileMapAsJsObject = this._tileMap.toJSObject();
syncData.tm = currentTileMapAsJsObject;
}
return syncData;
}
updateFromNetworkSyncData(
networkSyncData: SimpleTileMapNetworkSyncData
networkSyncData: SimpleTileMapNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (
networkSyncData.op !== undefined &&
networkSyncData.op !== this._opacity
) {
this.setOpacity(networkSyncData.op);
}
if (networkSyncData.ai !== undefined) {
// TODO: support changing the atlas texture
if (networkSyncData.tm !== undefined) {
this._loadTileMap(
networkSyncData.tm,
(tileMap: TileMapHelper.EditableTileMap) => {
if (networkSyncData.w !== undefined) {
this.setWidth(networkSyncData.w);
}
if (networkSyncData.h !== undefined) {
this.setHeight(networkSyncData.h);
}
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
}
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
if (this._collisionTileMap) {
// If collision tile map is already defined, only update it.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap =
new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
}
this.updateTransformation();
}
);
}
}
@@ -199,39 +228,43 @@ namespace gdjs {
// 2. Update the renderer so that it updates the tilemap object
// (used for width and position calculations).
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions & opacity if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this._loadTileMap(
this._initialTileMapAsJsObject,
(tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions & opacity if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
if (this._collisionTileMap) {
// If collision tile map is already defined, there's a good chance it means
// extraInitializationFromInitialInstance is called when hot reloading the
// scene so the collision is tile map is updated instead of being re-created.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
if (this._collisionTileMap) {
// If collision tile map is already defined, there's a good chance it means
// extraInitializationFromInitialInstance is called when hot reloading the
// scene so the collision is tile map is updated instead of being re-created.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap =
new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
}
this.updateTransformation();
}
this.updateTransformation();
});
);
}
private _loadInitialTileMap(
private _loadTileMap(
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
): void {
if (!this._initialTileMapAsJsObject) return;
if (this._columnCount <= 0 || this._rowCount <= 0) {
console.error(
`Tilemap object ${this.name} is not configured properly.`
@@ -240,7 +273,7 @@ namespace gdjs {
}
this._tileMapManager.getOrLoadSimpleTileMap(
this._initialTileMapAsJsObject,
tileMapAsJsObject,
this.name,
this._tileSize,
this._columnCount,
@@ -284,7 +317,8 @@ namespace gdjs {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
this._tileMap = tileMap;
this._renderer.refreshPixiTileMap(textureCache);
tileMapLoadingCallback(tileMap);
},
(error) => {
@@ -640,7 +674,7 @@ namespace gdjs {
}
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
return this._renderer.getTileId(columnIndex, rowIndex, 0);
return this.getTileId(columnIndex, rowIndex, 0);
}
setTileAtPosition(tileId: number, x: float, y: float) {
@@ -654,11 +688,10 @@ namespace gdjs {
columnIndex: integer,
rowIndex: integer
) {
const tileMap = this._renderer._tileMap;
if (!tileMap) {
if (!this._tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
const layer = this._tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
@@ -670,8 +703,8 @@ namespace gdjs {
if (this._collisionTileMap) {
const oldTileDefinition =
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
const newTileDefinition = tileMap.getTileDefinition(tileId);
oldTileId !== undefined && this._tileMap.getTileDefinition(oldTileId);
const newTileDefinition = this._tileMap.getTileDefinition(tileId);
const hadFullHitBox =
!!oldTileDefinition &&
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
@@ -707,7 +740,7 @@ namespace gdjs {
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnY(columnIndex, rowIndex, 0, flip);
this.flipTileOnY(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
@@ -718,7 +751,7 @@ namespace gdjs {
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnX(columnIndex, rowIndex, 0, flip);
this.flipTileOnX(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
@@ -728,22 +761,22 @@ namespace gdjs {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtPosition(x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
removeTileAtPosition(x: float, y: float) {
@@ -753,11 +786,10 @@ namespace gdjs {
}
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
const tileMap = this._renderer._tileMap;
if (!tileMap) {
if (!this._tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
const layer = this._tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
@@ -779,24 +811,28 @@ namespace gdjs {
setGridRowCount(targetRowCount: integer) {
if (targetRowCount <= 0) return;
this._renderer.setGridRowCount(targetRowCount);
if (!this._tileMap) return;
this._tileMap.setDimensionY(targetRowCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
setGridColumnCount(targetColumnCount: integer) {
if (targetColumnCount <= 0) return;
this._renderer.setGridColumnCount(targetColumnCount);
if (!this._tileMap) return;
this._tileMap.setDimensionX(targetColumnCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
getGridRowCount(): integer {
return this._renderer.getGridRowCount();
if (!this._tileMap) return 0;
return this._tileMap.getDimensionY();
}
getGridColumnCount(): integer {
return this._renderer.getGridColumnCount();
if (!this._tileMap) return 0;
return this._tileMap.getDimensionX();
}
getTilesetColumnCount(): integer {
@@ -806,6 +842,73 @@ namespace gdjs {
getTilesetRowCount(): integer {
return this._rowCount;
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
if (!this._tileMap) return -1;
return this._tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnY(x, y, layerIndex);
}
}
gdjs.registerObject(
'TileMap::SimpleTileMap',

View File

@@ -189,9 +189,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TilemapCollisionMaskNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TilemapCollisionMaskNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
tmjf: this.getTilemapJsonFile(),
tsjf: this.getTilesetJsonFile(),
dm: this.getDebugMode(),
@@ -204,9 +206,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TilemapCollisionMaskNetworkSyncData
networkSyncData: TilemapCollisionMaskNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.tmjf !== undefined) {
this.setTilemapJsonFile(networkSyncData.tmjf);

View File

@@ -10,8 +10,6 @@ namespace gdjs {
private _object:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject;
// TODO Move this attribute in the object as it's a model.
_tileMap: TileMapHelper.EditableTileMap | null = null;
private _pixiObject: PIXI.tilemap.CompositeTilemap;
@@ -51,40 +49,9 @@ namespace gdjs {
this._pixiObject.tileAnim[0] += 1;
}
updatePixiTileMap(
tileMap: TileMapHelper.EditableTileMap,
textureCache: TileMapHelper.TileTextureCache
) {
this._tileMap = tileMap;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
if (!this._tileMap) return;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
this._tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
updatePosition(): void {
this._pixiObject.pivot.x = this.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this.getTileMapHeight() / 2;
this._pixiObject.pivot.x = this._object.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this._object.getTileMapHeight() / 2;
this._pixiObject.position.x = this._object.x + this.getWidth() / 2;
this._pixiObject.position.y = this._object.y + this.getHeight() / 2;
}
@@ -98,7 +65,7 @@ namespace gdjs {
// opacity. Setting alpha on each layer tile might not be useful as
// each layer would be separately transparent instead of the whole tilemap.
this._pixiObject.alpha = this._object._opacity / 255;
const tileMap = this._tileMap;
const tileMap = this._object.getTileMap();
if (!tileMap) return;
for (const layer of tileMap.getLayers()) {
if (
@@ -114,44 +81,34 @@ namespace gdjs {
}
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
setWidth(width: float): void {
this._pixiObject.scale.x = width / this.getTileMapWidth();
this._pixiObject.scale.x = width / this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setHeight(height: float): void {
this._pixiObject.scale.y = height / this.getTileMapHeight();
this._pixiObject.scale.y = height / this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
setScaleX(scaleX: float): void {
this._pixiObject.scale.x = scaleX;
const width = scaleX * this.getTileMapWidth();
const width = scaleX * this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setScaleY(scaleY: float): void {
this._pixiObject.scale.y = scaleY;
const height = scaleY * this.getTileMapHeight();
const height = scaleY * this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
getWidth(): float {
return this.getTileMapWidth() * this._pixiObject.scale.x;
return this._object.getTileMapWidth() * this._pixiObject.scale.x;
}
getHeight(): float {
return this.getTileMapHeight() * this._pixiObject.scale.y;
return this._object.getTileMapHeight() * this._pixiObject.scale.y;
}
getScaleX(): float {
@@ -162,91 +119,17 @@ namespace gdjs {
return this._pixiObject.scale.y;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
const tileMap = this._tileMap;
if (!tileMap) return -1;
return tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
const tileMap = this._object.getTileMap();
if (!tileMap) return;
tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
if (!tileMap) return;
tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnY(x, y, layerIndex);
}
/**
* @param targetRowCount The number of rows to have.
*/
setGridRowCount(targetRowCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.setDimensionY(targetRowCount);
}
/**
* @param targetColumnCount The number of rows to have.
*/
setGridColumnCount(targetColumnCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.setDimensionX(targetColumnCount);
}
getGridRowCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionY();
}
getGridColumnCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionX();
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._displayMode,
this._object._layerIndex
);
}
destroy(): void {

View File

@@ -2,7 +2,6 @@
namespace gdjs {
export type TilemapObjectDataType = {
content: {
opacity: number;
tilemapJsonFile: string;
tilesetJsonFile: string;
tilemapAtlasImage: string;
@@ -38,7 +37,7 @@ namespace gdjs {
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler
{
_frameElapsedTime: float = 0;
_opacity: float;
_opacity: float = 255;
_tilemapJsonFile: string;
_tilesetJsonFile: string;
_tilemapAtlasImage: string;
@@ -48,11 +47,14 @@ namespace gdjs {
_animationSpeedScale: number;
_animationFps: number;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_tileMap: TileMapHelper.EditableTileMap | null = null;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
constructor(instanceContainer: gdjs.RuntimeInstanceContainer, objectData) {
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: TilemapObjectData
) {
super(instanceContainer, objectData);
this._opacity = objectData.content.opacity;
this._tilemapJsonFile = objectData.content.tilemapJsonFile;
this._tilesetJsonFile = objectData.content.tilesetJsonFile;
this._tilemapAtlasImage = objectData.content.tilemapAtlasImage;
@@ -93,9 +95,6 @@ namespace gdjs {
oldObjectData: TilemapObjectData,
newObjectData: TilemapObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (
oldObjectData.content.tilemapJsonFile !==
newObjectData.content.tilemapJsonFile
@@ -145,9 +144,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TilemapNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TilemapNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
tmjf: this._tilemapJsonFile,
tsjf: this._tilesetJsonFile,
@@ -159,8 +160,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: TilemapNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: TilemapNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
@@ -230,7 +234,9 @@ namespace gdjs {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
this._tileMap = tileMap;
this._renderer.refreshPixiTileMap(textureCache);
this.invalidateHitboxes();
}
);
}
@@ -427,6 +433,73 @@ namespace gdjs {
getScaleY(): float {
return this._renderer.getScaleY();
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
if (!this._tileMap) return -1;
return this._tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnY(x, y, layerIndex);
}
}
gdjs.registerObject('TileMap::TileMap', gdjs.TileMapRuntimeObject);
}

View File

@@ -78,9 +78,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TiledSpriteNetworkSyncData {
getNetworkSyncData(
syncOptons: GetNetworkSyncDataOptions
): TiledSpriteNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptons),
xo: this.getXOffset(),
yo: this.getYOffset(),
op: this.getOpacity(),
@@ -89,9 +91,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TiledSpriteNetworkSyncData
networkSyncData: TiledSpriteNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -63,7 +63,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
_dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
// This is useful when the object is synchronized over the network,
// object is controlled by the network and we want to ensure the current player
// cannot control it.
@@ -109,14 +109,16 @@ namespace gdjs {
: behaviorData.useLegacyTurnBack;
}
getNetworkSyncData(): TopDownMovementNetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): TopDownMovementNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
this._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
a: this._angle,
xv: this._xVelocity,
@@ -134,9 +136,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TopDownMovementNetworkSyncData
networkSyncData: TopDownMovementNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.a !== undefined) {
@@ -173,10 +176,10 @@ namespace gdjs {
this._stickForce = behaviorSpecificProps.sf;
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// And we are not using the default controls.
this._ignoreDefaultControlsAsSyncedByNetwork = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
// And ignore default controls if not asked otherwise.
this._ignoreDefaultControlsAsSyncedByNetwork = !options.keepControl;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -581,7 +584,7 @@ namespace gdjs {
this._wasRightKeyPressed = this._rightKey;
this._wasUpKeyPressed = this._upKey;
this._wasDownKeyPressed = this._downKey;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._leftKey = false;
this._rightKey = false;
this._upKey = false;

File diff suppressed because it is too large Load Diff

View File

@@ -347,17 +347,21 @@ describe('gdjs.TweenRuntimeBehavior', () => {
// Check that there is no NaN.
for (let i = 0; i < 11; i++) {
runtimeScene.renderAndStep(1000 / 60);
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
// The tween tries to set the camera zoom to 0, but it has no effect
// because it doesn't make sense.
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
}
});
it('can tween a layer camera zoom from 0', () => {
// The zoom stays at 1 because 0 doesn't make sense.
camera.setCameraZoom(runtimeScene, 0, '', 0);
// Here, it actually tweens from 1 to 1.
tween.tweenCameraZoom2(runtimeScene, 'MyTween', 1, '', 'linear', 0.25);
// A camera zoom of 0 doesn't make sense.
// Check that there is no NaN.
for (let i = 0; i < 11; i++) {
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
runtimeScene.renderAndStep(1000 / 60);
}
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);

View File

@@ -4,6 +4,15 @@ Copyright (c) 2010-2023 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('Tween');
interface TweenBehaviorNetworkSyncDataType {
tweenManager: TweenManagerNetworkSyncData;
}
export interface TweenBehaviorNetworkSyncData
extends BehaviorNetworkSyncData {
props: TweenBehaviorNetworkSyncDataType;
}
interface IColorable extends gdjs.RuntimeObject {
setColor(color: string): void;
getColor(): string;
@@ -59,6 +68,305 @@ namespace gdjs {
const exponentialInterpolation =
gdjs.evtTools.common.exponentialInterpolation;
const tweenObjectValueSetter = () => {};
const getTweenVariableSetter = (variable: gdjs.Variable) => {
return (value: float) => variable.setNumber(value);
};
const getTweenObjectPositionXSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setX(value);
};
const getTweenObjectPositionYSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setY(value);
};
const getTweenObjectPositionZSetter = (object: gdjs.RuntimeObject) => {
if (!is3D(object)) return () => {};
return (value: float) => object.setZ(value);
};
const getTweenObjectPositionSetter = (object: gdjs.RuntimeObject) => {
return ([x, y]: float[]) => object.setPosition(x, y);
};
const getTweenObjectAngleSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setAngle(value);
};
const getTweenObjectWidthSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setWidth(value);
};
const getTweenObjectHeightSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setHeight(value);
};
const getTweenObjectRotationXSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setRotationX(value);
};
const getTweenObjectRotationYSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setRotationY(value);
};
const getTweenObjectDepthSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setDepth(value);
};
const getTweenObjectScaleXYSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? ([scaleX, scaleY]: float[]) => {
const oldX = object.getCenterXInScene();
const oldY = object.getCenterYInScene();
object.setScaleX(scaleX);
object.setScaleY(scaleY);
object.setCenterPositionInScene(oldX, oldY);
}
: ([scaleX, scaleY]: float[]) => {
object.setScaleX(scaleX);
object.setScaleY(scaleY);
};
};
const getTweenObjectScaleSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
object3d: (gdjs.RuntimeObject & gdjs.Base3DHandler) | null,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scale: float) => {
const oldX = object.getCenterXInScene();
const oldY = object.getCenterYInScene();
const oldZ = object3d ? object3d.getCenterZInScene() : 0;
object.setScale(scale);
object.setCenterXInScene(oldX);
object.setCenterYInScene(oldY);
if (object3d) {
object3d.setCenterZInScene(oldZ);
}
}
: (scale: float) => object.setScale(scale);
};
const getTweenObjectScaleXSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scaleX: float) => {
const oldX = object.getCenterXInScene();
object.setScaleX(scaleX);
object.setCenterXInScene(oldX);
}
: (scaleX: float) => object.setScaleX(scaleX);
};
const getTweenObjectScaleYSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scaleY: float) => {
const oldY = object.getCenterYInScene();
object.setScaleY(scaleY);
object.setCenterYInScene(oldY);
}
: (scaleY: float) => object.setScaleY(scaleY);
};
const getTweenObjectOpacitySetter = (
object: gdjs.RuntimeObject & gdjs.OpacityHandler
) => {
return (value: float) => object.setOpacity(value);
};
const getTweenObjectCharacterSizeSetter = (object: ICharacterScalable) => {
return (value: float) => object.setCharacterSize(value);
};
const getTweenObjectNumberEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return (value: float) => {
effect.updateDoubleParameter(propertyName, value);
};
};
const getTweenObjectColorEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
};
};
const getTweenObjectColorSetter = (
object: IColorable,
useHSLColorTransition: boolean
) => {
if (useHSLColorTransition) {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
object.setColor(
Math.floor(rgbFromHslColor[0]) +
';' +
Math.floor(rgbFromHslColor[1]) +
';' +
Math.floor(rgbFromHslColor[2])
);
};
} else {
return ([red, green, blue]: number[]) => {
object.setColor(
Math.floor(red) + ';' + Math.floor(green) + ';' + Math.floor(blue)
);
};
}
};
const getTweenObjectColorHSLSetter = (object: IColorable) => {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
object.setColor(
Math.floor(rgbFromHslColor[0]) +
';' +
Math.floor(rgbFromHslColor[1]) +
';' +
Math.floor(rgbFromHslColor[2])
);
};
};
const tweenSetterFactory =
(object: RuntimeObject) =>
(tweenInformation: TweenInformationNetworkSyncData) => {
const type = tweenInformation.type;
const variablePath = tweenInformation.variablePath;
const effectName = tweenInformation.effectName;
const propertyName = tweenInformation.propertyName;
const scaleFromCenterOfObject =
!!tweenInformation.scaleFromCenterOfObject;
const useHSLColorTransition = !!tweenInformation.useHSLColorTransition;
if (type === 'objectValue') {
return tweenObjectValueSetter;
}
if (type === 'variable' && variablePath) {
const variable = object
.getVariables()
.getVariableFromPath(variablePath);
if (!variable) return () => {};
return getTweenVariableSetter(variable);
}
if (type === 'positionX') {
return getTweenObjectPositionXSetter(object);
}
if (type === 'positionY') {
return getTweenObjectPositionYSetter(object);
}
if (type === 'position') {
return getTweenObjectPositionSetter(object);
}
if (type === 'positionZ') {
return getTweenObjectPositionZSetter(object);
}
if (type === 'width') {
return getTweenObjectWidthSetter(object);
}
if (type === 'height') {
return getTweenObjectHeightSetter(object);
}
if (type === 'depth') {
if (!is3D(object)) return () => {};
return getTweenObjectDepthSetter(object);
}
if (type === 'angle') {
return getTweenObjectAngleSetter(object);
}
if (type === 'rotationX') {
if (!is3D(object)) return () => {};
return getTweenObjectRotationXSetter(object);
}
if (type === 'rotationY') {
if (!is3D(object)) return () => {};
return getTweenObjectRotationYSetter(object);
}
if (type === 'scale') {
if (!isScalable(object)) return () => {};
const object3d = is3D(object) ? object : null;
return getTweenObjectScaleSetter(
object,
object3d,
scaleFromCenterOfObject
);
}
if (type === 'scaleXY') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleXYSetter(object, scaleFromCenterOfObject);
}
if (type === 'scaleX') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleXSetter(object, scaleFromCenterOfObject);
}
if (type === 'scaleY') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleYSetter(object, scaleFromCenterOfObject);
}
if (type === 'opacity') {
if (!isOpaque(object)) return () => {};
return getTweenObjectOpacitySetter(object);
}
if (type === 'characterSize') {
if (!isCharacterScalable(object)) return () => {};
return getTweenObjectCharacterSizeSetter(object);
}
if (type === 'numberEffectProperty' && effectName && propertyName) {
const effect = object.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The object "${object.name}" doesn't have any effect called "${effectName}"`
);
}
return getTweenObjectNumberEffectPropertySetter(effect, propertyName);
}
if (type === 'colorEffectProperty' && effectName && propertyName) {
const effect = object.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The object "${object.name}" doesn't have any effect called "${effectName}"`
);
}
return getTweenObjectColorEffectPropertySetter(effect, propertyName);
}
if (type === 'objectColor') {
if (!isColorable(object)) return () => {};
return getTweenObjectColorSetter(object, useHSLColorTransition);
}
if (type === 'objectColorHSL') {
if (!isColorable(object)) return () => {};
return getTweenObjectColorHSLSetter(object);
}
return () => {};
};
export class TweenRuntimeBehavior extends gdjs.RuntimeBehavior {
private _tweens = new gdjs.evtTools.tween.TweenManager();
private _isActive: boolean = true;
@@ -84,6 +392,43 @@ namespace gdjs {
return true;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TweenBehaviorNetworkSyncData {
return {
...super.getNetworkSyncData(syncOptions),
props: {
tweenManager: this._tweens.getNetworkSyncData(),
},
};
}
updateFromNetworkSyncData(
networkSyncData: TweenBehaviorNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.props.tweenManager) {
this._tweens.updateFromNetworkSyncData(
networkSyncData.props.tweenManager,
(tweenInformationNetworkSyncData) => {
return this.owner;
},
(tweenInformationNetworkSyncData) => {
return tweenSetterFactory(this.owner)(
tweenInformationNetworkSyncData
);
},
(tweenInformationNetworkSyncData) => {
return tweenInformationNetworkSyncData.destroyObjectWhenFinished
? () => this._deleteFromScene()
: null;
}
);
}
}
doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer): void {
this._tweens.step();
}
@@ -126,7 +471,12 @@ namespace gdjs {
linearInterpolation,
fromValue,
toValue,
(value: float) => variable.setNumber(value),
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -208,7 +558,12 @@ namespace gdjs {
linearInterpolation,
variable.getValue() as number,
toValue,
(value: float) => variable.setNumber(value),
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -243,7 +598,11 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {},
tweenObjectValueSetter,
{
type: 'objectValue',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -322,7 +681,11 @@ namespace gdjs {
linearInterpolation,
[this.owner.getX(), this.owner.getY()],
[toX, toY],
([x, y]) => this.owner.setPosition(x, y),
getTweenObjectPositionSetter(this.owner),
{
type: 'position',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -394,7 +757,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getX(),
toX,
(value: float) => this.owner.setX(value),
getTweenObjectPositionXSetter(this.owner),
{
type: 'positionX',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -466,7 +833,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getY(),
toY,
(value: float) => this.owner.setY(value),
getTweenObjectPositionYSetter(this.owner),
{
type: 'positionY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -543,7 +914,11 @@ namespace gdjs {
linearInterpolation,
owner.getZ(),
toZ,
(value: float) => owner.setZ(value),
getTweenObjectPositionZSetter(owner),
{
type: 'positionZ',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -615,7 +990,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getAngle(),
toAngle,
(value: float) => this.owner.setAngle(value),
getTweenObjectAngleSetter(this.owner),
{
type: 'angle',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -648,7 +1027,11 @@ namespace gdjs {
linearInterpolation,
owner.getRotationX(),
toAngle,
(value: float) => owner.setRotationX(value),
getTweenObjectRotationXSetter(owner),
{
type: 'rotationX',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -681,7 +1064,11 @@ namespace gdjs {
linearInterpolation,
owner.getRotationY(),
toAngle,
(value: float) => owner.setRotationY(value),
getTweenObjectRotationYSetter(owner),
{
type: 'rotationY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -769,19 +1156,6 @@ namespace gdjs {
if (toScaleX < 0) toScaleX = 0;
if (toScaleY < 0) toScaleY = 0;
const setValue = scaleFromCenterOfObject
? ([scaleX, scaleY]: float[]) => {
const oldX = owner.getCenterXInScene();
const oldY = owner.getCenterYInScene();
owner.setScaleX(scaleX);
owner.setScaleY(scaleY);
owner.setCenterPositionInScene(oldX, oldY);
}
: ([scaleX, scaleY]: float[]) => {
owner.setScaleX(scaleX);
owner.setScaleY(scaleY);
};
this._tweens.addMultiTween(
identifier,
timeSource,
@@ -790,7 +1164,11 @@ namespace gdjs {
interpolation,
[owner.getScaleX(), owner.getScaleY()],
[toScaleX, toScaleY],
setValue,
getTweenObjectScaleXYSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleXY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -830,20 +1208,6 @@ namespace gdjs {
// when the 3D extension is not used.
const owner3d = is3D(owner) ? owner : null;
const setValue = scaleFromCenterOfObject
? (scale: float) => {
const oldX = owner.getCenterXInScene();
const oldY = owner.getCenterYInScene();
const oldZ = owner3d ? owner3d.getCenterZInScene() : 0;
owner.setScale(scale);
owner.setCenterXInScene(oldX);
owner.setCenterYInScene(oldY);
if (owner3d) {
owner3d.setCenterZInScene(oldZ);
}
}
: (scale: float) => owner.setScale(scale);
this._tweens.addSimpleTween(
identifier,
this.owner,
@@ -852,7 +1216,12 @@ namespace gdjs {
exponentialInterpolation,
owner.getScale(),
toScale,
setValue,
getTweenObjectScaleSetter(owner, owner3d, scaleFromCenterOfObject),
{
type: 'scale',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -929,14 +1298,6 @@ namespace gdjs {
const owner = this.owner;
if (!isScalable(owner)) return;
const setValue = scaleFromCenterOfObject
? (scaleX: float) => {
const oldX = owner.getCenterXInScene();
owner.setScaleX(scaleX);
owner.setCenterXInScene(oldX);
}
: (scaleX: float) => owner.setScaleX(scaleX);
this._tweens.addSimpleTween(
identifier,
timeSource,
@@ -945,7 +1306,12 @@ namespace gdjs {
interpolation,
owner.getScaleX(),
toScaleX,
setValue,
getTweenObjectScaleXSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleX',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1022,14 +1388,6 @@ namespace gdjs {
const owner = this.owner;
if (!isScalable(owner)) return;
const setValue = scaleFromCenterOfObject
? (scaleY: float) => {
const oldY = owner.getCenterYInScene();
owner.setScaleY(scaleY);
owner.setCenterYInScene(oldY);
}
: (scaleY: float) => owner.setScaleY(scaleY);
this._tweens.addSimpleTween(
identifier,
timeSource,
@@ -1038,7 +1396,12 @@ namespace gdjs {
interpolation,
owner.getScaleY(),
toScaleY,
setValue,
getTweenObjectScaleYSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleY',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1113,7 +1476,11 @@ namespace gdjs {
linearInterpolation,
owner.getOpacity(),
toOpacity,
(value: float) => owner.setOpacity(value),
getTweenObjectOpacitySetter(owner),
{
type: 'opacity',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1153,10 +1520,12 @@ namespace gdjs {
linearInterpolation,
effect ? effect.getDoubleParameter(propertyName) : 0,
toValue,
(value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
getTweenObjectNumberEffectPropertySetter(effect, propertyName),
{
type: 'numberEffectProperty',
effectName,
propertyName,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
@@ -1210,22 +1579,12 @@ namespace gdjs {
rgbToColor[1],
rgbToColor[2]
),
([hue, saturation, lightness]) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
getTweenObjectColorEffectPropertySetter(effect, propertyName),
{
type: 'colorEffectProperty',
effectName,
propertyName,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
@@ -1305,43 +1664,20 @@ namespace gdjs {
const rgbFromColor: float[] = gdjs.rgbOrHexToRGBColor(owner.getColor());
const rgbToColor: float[] = gdjs.rgbOrHexToRGBColor(toColorStr);
let initialValue;
let targetedValue;
let setValue;
if (useHSLColorTransition) {
initialValue = gdjs.evtTools.tween.rgbToHsl(
rgbFromColor[0],
rgbFromColor[1],
rgbFromColor[2]
);
targetedValue = gdjs.evtTools.tween.rgbToHsl(
rgbToColor[0],
rgbToColor[1],
rgbToColor[2]
);
setValue = ([hue, saturation, lightness]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
owner.setColor(
Math.round(rgbFromHslColor[0]) +
';' +
Math.round(rgbFromHslColor[1]) +
';' +
Math.round(rgbFromHslColor[2])
);
};
} else {
initialValue = rgbFromColor;
targetedValue = rgbToColor;
setValue = ([red, green, blue]) => {
owner.setColor(
Math.floor(red) + ';' + Math.floor(green) + ';' + Math.floor(blue)
);
};
}
const initialValue = useHSLColorTransition
? gdjs.evtTools.tween.rgbToHsl(
rgbFromColor[0],
rgbFromColor[1],
rgbFromColor[2]
)
: rgbFromColor;
const targetedValue = useHSLColorTransition
? gdjs.evtTools.tween.rgbToHsl(
rgbToColor[0],
rgbToColor[1],
rgbToColor[2]
)
: rgbToColor;
this._tweens.addMultiTween(
identifier,
@@ -1351,7 +1687,12 @@ namespace gdjs {
linearInterpolation,
initialValue,
targetedValue,
setValue,
getTweenObjectColorSetter(owner, useHSLColorTransition),
{
type: 'objectColor',
useHSLColorTransition,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1462,25 +1803,13 @@ namespace gdjs {
duration,
easing,
linearInterpolation,
hslFromColor,
[toH, toS, toL],
([hue, saturation, lightness]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
owner.setColor(
Math.round(rgbFromHslColor[0]) +
';' +
Math.round(rgbFromHslColor[1]) +
';' +
Math.round(rgbFromHslColor[2])
);
getTweenObjectColorHSLSetter(owner),
{
type: 'objectColorHSL',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1558,7 +1887,11 @@ namespace gdjs {
interpolation,
owner.getCharacterSize(),
toSize,
(value: float) => owner.setCharacterSize(value),
getTweenObjectCharacterSizeSetter(owner),
{
type: 'characterSize',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1630,7 +1963,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getWidth(),
toWidth,
(value: float) => this.owner.setWidth(value),
getTweenObjectWidthSetter(this.owner),
{
type: 'width',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1702,7 +2039,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getHeight(),
toHeight,
(value: float) => this.owner.setHeight(value),
getTweenObjectHeightSetter(this.owner),
{
type: 'height',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1779,7 +2120,11 @@ namespace gdjs {
linearInterpolation,
owner.getDepth(),
toDepth,
(value: float) => owner.setDepth(value),
getTweenObjectDepthSetter(owner),
{
type: 'depth',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}

View File

@@ -10,6 +10,127 @@ namespace gdjs {
export namespace tween {
const logger = new gdjs.Logger('Tween');
const getTweenVariableSetter = (variable: gdjs.Variable) => {
return (value: float) => variable.setNumber(value);
};
const tweenLayoutValueSetter = (value: float) => {};
const tweenLayerValueSetter = (value: float) => {};
const getTweenLayerCameraPositionSetter = (layer: gdjs.RuntimeLayer) => {
return ([x, y]: Array<float>) => {
layer.setCameraX(x);
layer.setCameraY(y);
};
};
const getTweenLayerCameraRotationSetter = (layer: gdjs.RuntimeLayer) => {
return (value: float) => layer.setCameraRotation(value);
};
const getTweenLayerCameraZoomSetter = (layer: gdjs.RuntimeLayer) => {
return (value: float) => layer.setCameraZoom(value);
};
const getTweenNumberEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return (value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
};
};
const getTweenColorEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return ([hue, saturation, lightness]: Array<float>) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
};
};
// Factory to get the tween setter based on type and options
export const tweenSetterFactory =
(runtimeScene: RuntimeScene) =>
(tweenInformation: TweenInformationNetworkSyncData) => {
const type = tweenInformation.type;
const layerName = tweenInformation.layerName;
const variablePath = tweenInformation.variablePath;
const effectName = tweenInformation.effectName;
const propertyName = tweenInformation.propertyName;
if (type === 'variable' && variablePath) {
const variable = runtimeScene
.getVariables()
.getVariableFromPath(variablePath);
if (!variable) {
return () => {};
}
return getTweenVariableSetter(variable);
}
if (type === 'cameraZoom' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraZoomSetter(layer);
}
if (type === 'cameraRotation' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraRotationSetter(layer);
}
if (type === 'cameraPosition' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraPositionSetter(layer);
}
if (
type === 'colorEffectProperty' &&
layerName !== undefined &&
effectName &&
propertyName
) {
const layer = runtimeScene.getLayer(layerName);
const effect = layer.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The layer "${layerName}" doesn't have any effect called "${effectName}"`
);
}
return getTweenColorEffectPropertySetter(effect, propertyName);
}
if (
type === 'numberEffectProperty' &&
layerName !== undefined &&
effectName &&
propertyName
) {
const layer = runtimeScene.getLayer(layerName);
const effect = layer.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The layer "${layerName}" doesn't have any effect called "${effectName}"`
);
}
return getTweenNumberEffectPropertySetter(effect, propertyName);
}
if (type === 'layoutValue') {
return tweenLayoutValueSetter;
}
if (type === 'layerValue') {
return tweenLayerValueSetter;
}
return () => {};
};
export const getTweensMap = (runtimeScene: RuntimeScene) =>
runtimeScene._tweens ||
(runtimeScene._tweens = new gdjs.evtTools.tween.TweenManager());
@@ -20,6 +141,44 @@ namespace gdjs {
gdjs.evtTools.tween.getTweensMap(runtimeScene).step();
});
gdjs.registerRuntimeSceneGetSyncDataCallback(
function (runtimeScene, currentLayoutSyncData, syncOptions) {
if (!syncOptions.syncTweens) return;
const tweensNetworkSyncData = gdjs.evtTools.tween
.getTweensMap(runtimeScene)
.getNetworkSyncData();
currentLayoutSyncData.tween = tweensNetworkSyncData;
}
);
gdjs.registerRuntimeSceneUpdateFromSyncDataCallback(
function (runtimeScene, receivedSyncData, syncOptions) {
if (!receivedSyncData.tween) return;
gdjs.evtTools.tween
.getTweensMap(runtimeScene)
.updateFromNetworkSyncData(
receivedSyncData.tween,
(tweenInformationNetworkSyncData) => {
if (tweenInformationNetworkSyncData.layerName !== undefined) {
return runtimeScene.getLayer(
tweenInformationNetworkSyncData.layerName
);
}
return runtimeScene;
},
(tweenInformationNetworkSyncData) => {
return gdjs.evtTools.tween.tweenSetterFactory(runtimeScene)(
tweenInformationNetworkSyncData
);
},
// No onFinish for scene tweens.
() => null
);
}
);
export const sceneTweenExists = (
runtimeScene: RuntimeScene,
id: string
@@ -130,7 +289,10 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {}
tweenLayoutValueSetter,
{
type: 'layoutValue',
}
);
};
@@ -167,7 +329,11 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {}
tweenLayerValueSetter,
{
type: 'layerValue',
layerName,
}
);
};
@@ -197,7 +363,11 @@ namespace gdjs {
linearInterpolation,
from,
to,
(value: float) => variable.setNumber(value)
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
}
);
};
@@ -250,7 +420,11 @@ namespace gdjs {
linearInterpolation,
variable.getValue() as number,
toValue,
(value: float) => variable.setNumber(value)
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
}
);
};
@@ -329,9 +503,10 @@ namespace gdjs {
linearInterpolation,
[layer.getCameraX(), layer.getCameraY()],
[toX, toY],
([x, y]) => {
layer.setCameraX(x);
layer.setCameraY(y);
getTweenLayerCameraPositionSetter(layer),
{
type: 'cameraPosition',
layerName,
}
);
};
@@ -408,7 +583,11 @@ namespace gdjs {
interpolation,
layer.getCameraZoom(),
toZoom,
(value: float) => layer.setCameraZoom(value)
getTweenLayerCameraZoomSetter(layer),
{
type: 'cameraZoom',
layerName,
}
);
};
@@ -478,7 +657,11 @@ namespace gdjs {
linearInterpolation,
layer.getCameraRotation(),
toRotation,
(value: float) => layer.setCameraRotation(value)
getTweenLayerCameraRotationSetter(layer),
{
type: 'cameraRotation',
layerName,
}
);
};
@@ -518,10 +701,12 @@ namespace gdjs {
linearInterpolation,
effect ? effect.getDoubleParameter(propertyName) : 0,
toValue,
(value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
getTweenNumberEffectPropertySetter(effect, propertyName),
{
type: 'numberEffectProperty',
layerName,
effectName,
propertyName,
}
);
};
@@ -575,22 +760,12 @@ namespace gdjs {
rgbToColor[1],
rgbToColor[2]
),
([hue, saturation, lightness]) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
getTweenColorEffectPropertySetter(effect, propertyName),
{
type: 'colorEffectProperty',
layerName,
effectName,
propertyName,
}
);
};

View File

@@ -16,7 +16,7 @@ namespace gdjs {
export type VideoObjectData = ObjectData & VideoObjectDataType;
export type VideoNetworkSyncDataType = {
export type VideoObjectNetworkSyncDataType = {
op: float;
// We don't sync volume, as it's probably a user setting?
pla: boolean;
@@ -25,8 +25,8 @@ namespace gdjs {
ps: number;
};
export type VideoNetworkSyncData = ObjectNetworkSyncData &
VideoNetworkSyncDataType;
export type VideoObjectNetworkSyncData = ObjectNetworkSyncData &
VideoObjectNetworkSyncDataType;
/**
* An object displaying a video on screen.
@@ -99,9 +99,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): VideoNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): VideoObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
pla: this.isPlayed(),
loop: this.isLooped(),
@@ -110,8 +112,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: VideoNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: VideoObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (this._opacity !== undefined && this._opacity && syncData.op) {
this.setOpacity(syncData.op);

View File

@@ -214,16 +214,16 @@ CODE_NAMESPACE.RUNTIME_BEHAVIOR_CLASSNAME = class RUNTIME_BEHAVIOR_CLASSNAME ext
}
// Network sync:
getNetworkSyncData() {
getNetworkSyncData(syncOptions) {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
GET_NETWORK_SYNC_DATA_CODE
}
};
}
updateFromNetworkSyncData(networkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(networkSyncData, options) {
super.updateFromNetworkSyncData(networkSyncData, options);
UPDATE_FROM_NETWORK_SYNC_DATA_CODE
}

View File

@@ -74,10 +74,15 @@ gd::String EventsCodeGenerator::GenerateEventsListCompleteFunctionCode(
codeGenerator.GetCodeNamespace() + ".localVariables = [];\n";
}
gd::String idToCallbackMapCode;
idToCallbackMapCode +=
codeGenerator.GetCodeNamespace() + ".idToCallbackMap = new Map();\n";
gd::String output =
// clang-format off
codeGenerator.GetCodeNamespace() + " = {};\n" +
localVariablesInitializationCode +
idToCallbackMapCode +
globalDeclarations +
globalObjectLists + "\n\n" +
codeGenerator.GetCustomCodeOutsideMain() + "\n\n" +
@@ -135,8 +140,11 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionCode(
gd::VariablesContainer::SourceType::Parameters);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForFreeEventsFunction(
project, eventsFunctionsExtension, eventsFunction,
parameterObjectsAndGroups, parameterVariablesContainer);
project,
eventsFunctionsExtension,
eventsFunction,
parameterObjectsAndGroups,
parameterVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
codeGenerator.SetCodeNamespace(codeNamespace);
@@ -145,16 +153,24 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionCode(
gd::DiagnosticReport diagnosticReport;
codeGenerator.SetDiagnosticReport(&diagnosticReport);
// Generate the code setting up the context of the function.
gd::String fullPreludeCode = "let scopeInstanceContainer = null;\n" +
codeGenerator.GenerateFreeEventsFunctionContext(
eventsFunctionsExtension,
eventsFunction,
"runtimeScene.getOnceTriggers()");
gd::String output = GenerateEventsListCompleteFunctionCode(
codeGenerator, codeGenerator.GetCodeNamespaceAccessor() + "func",
codeGenerator,
codeGenerator.GetCodeNamespaceAccessor() + "func",
codeGenerator.GenerateEventsFunctionParameterDeclarationsList(
eventsFunction.GetParametersForEvents(
eventsFunctionsExtension.GetEventsFunctions()),
0, true),
codeGenerator.GenerateFreeEventsFunctionContext(
eventsFunctionsExtension, eventsFunction,
"runtimeScene.getOnceTriggers()"),
eventsFunction.GetEvents(), "",
0,
true),
fullPreludeCode,
eventsFunction.GetEvents(),
"",
codeGenerator.GenerateEventsFunctionReturn(eventsFunction));
// TODO: the editor should pass the diagnostic report and display it to the
@@ -190,9 +206,13 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionCode(
gd::VariablesContainer::SourceType::Properties);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForBehaviorEventsFunction(
project, eventsFunctionsExtension, eventsBasedBehavior,
eventsFunction, parameterObjectsContainers,
parameterVariablesContainer, propertyVariablesContainer);
project,
eventsFunctionsExtension,
eventsBasedBehavior,
eventsFunction,
parameterObjectsContainers,
parameterVariablesContainer,
propertyVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
codeGenerator.SetCodeNamespace(codeNamespace);
@@ -206,9 +226,11 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionCode(
preludeCode + "\n" + "var that = this;\n" +
// runtimeScene is supposed to be always accessible, read
// it from the behavior.
// TODO: this should be renamed to "instanceContainer" and have the code generation
// adapted for this (rely less on `gdjs.RuntimeScene`, and more on `RuntimeInstanceContainer`).
// TODO: this should be renamed to "instanceContainer" and have the code
// generation adapted for this (rely less on `gdjs.RuntimeScene`, and more
// on `RuntimeInstanceContainer`).
"var runtimeScene = this._runtimeScene;\n" +
"let scopeInstanceContainer = null;\n" +
// By convention of Behavior Events Function, the object is accessible
// as a parameter called "Object", and thisObjectList is an array
// containing it (for faster access, without having to go through the
@@ -275,8 +297,12 @@ gd::String EventsCodeGenerator::GenerateObjectEventsFunctionCode(
gd::VariablesContainer::SourceType::Properties);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForObjectEventsFunction(
project, eventsFunctionsExtension, eventsBasedObject, eventsFunction,
parameterObjectsContainers, parameterVariablesContainer,
project,
eventsFunctionsExtension,
eventsBasedObject,
eventsFunction,
parameterObjectsContainers,
parameterVariablesContainer,
propertyVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
@@ -291,9 +317,11 @@ gd::String EventsCodeGenerator::GenerateObjectEventsFunctionCode(
preludeCode + "\n" + "var that = this;\n" +
// runtimeScene is supposed to be always accessible, read
// it from the object.
// TODO: this should be renamed to "instanceContainer" and have the code generation
// adapted for this (rely less on `gdjs.RuntimeScene`, and more on `RuntimeInstanceContainer`).
// TODO: this should be renamed to "instanceContainer" and have the code
// generation adapted for this (rely less on `gdjs.RuntimeScene`, and more
// on `RuntimeInstanceContainer`).
"var runtimeScene = this._instanceContainer;\n" +
"let scopeInstanceContainer = this._instanceContainer;\n" +
// By convention of Object Events Function, the object is accessible
// as a parameter called "Object", and thisObjectList is an array
// containing it (for faster access, without having to go through the
@@ -383,13 +411,14 @@ gd::String EventsCodeGenerator::GenerateFreeEventsFunctionContext(
gd::String objectsGettersMap;
gd::String objectArraysMap;
gd::String behaviorNamesMap;
return GenerateEventsFunctionContext(eventsFunctionsExtension,
eventsFunctionsExtension.GetEventsFunctions(),
eventsFunction,
onceTriggersVariable,
objectsGettersMap,
objectArraysMap,
behaviorNamesMap);
return GenerateEventsFunctionContext(
eventsFunctionsExtension,
eventsFunctionsExtension.GetEventsFunctions(),
eventsFunction,
onceTriggersVariable,
objectsGettersMap,
objectArraysMap,
behaviorNamesMap);
}
gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionContext(
@@ -624,7 +653,11 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionContext(
// the cost of creating/storing it for each events function might not
// be worth it.
" if (objectsList) {\n" +
" const object = parentEventsFunctionContext ?\n" +
" const object = parentEventsFunctionContext && "
// Check if `objectName` is not a child object and is passed by
// parameter from a parent instance container.
"!(scopeInstanceContainer && "
"scopeInstanceContainer.isObjectRegistered(objectName)) ?\n" +
" "
"parentEventsFunctionContext.createObject(objectsList.firstKey()) "
":\n" +
@@ -634,7 +667,7 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionContext(
" objectsList.get(objectsList.firstKey()).push(object);\n" +
" "
"eventsFunctionContext._objectArraysMap[objectName].push(object);\n" +
" }\n" + " return object;" + " }\n" +
" }\n" + " return object;\n" + " }\n" +
// Unknown object, don't create anything:
" return null;\n" +
" },\n"
@@ -646,7 +679,11 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionContext(
"eventsFunctionContext._objectsMap[objectName];\n" +
" let count = 0;\n" + " if (objectsList) {\n" +
" for(const objectName in objectsList.items)\n" +
" count += parentEventsFunctionContext ?\n" +
" count += parentEventsFunctionContext && "
// Check if `objectName` is not a child object and is passed by
// parameter from a parent instance container.
"!(scopeInstanceContainer && "
"scopeInstanceContainer.isObjectRegistered(objectName)) ?\n" +
"parentEventsFunctionContext.getInstancesCountOnScene(objectName) "
":\n" +
" runtimeScene.getInstancesCountOnScene(objectName);\n" +
@@ -916,7 +953,8 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
// Prepare call
@@ -965,10 +1003,11 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
actionCode += " " + call + ";\n";
actionCode += "}\n";
if (!optionalAsyncCallbackName.empty()) {
if (!optionalAsyncCallbackName.empty() && !optionalAsyncCallbackId.empty()) {
actionCode +=
"runtimeScene.getAsyncTasksManager().addTask(asyncTaskGroup, " +
optionalAsyncCallbackName + ")\n}";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList)\n}";
}
return actionCode;
@@ -982,7 +1021,8 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
// Prepare call
@@ -1042,10 +1082,12 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
actionCode += " " + call + ";\n";
actionCode += "}\n";
if (!optionalAsyncCallbackName.empty()) {
if (!optionalAsyncCallbackName.empty() &&
!optionalAsyncCallbackId.empty()) {
actionCode +=
"runtimeScene.getAsyncTasksManager().addTask(asyncTaskGroup, " +
optionalAsyncCallbackName + ");\n };";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList);\n };";
}
}
@@ -1364,17 +1406,16 @@ gd::String EventsCodeGenerator::GenerateGetVariable(
scope == VARIABLE_OR_PROPERTY_OR_PARAMETER) {
const auto variablesContainersList =
GetProjectScopedContainers().GetVariablesContainersList();
const auto &variablesContainer =
const auto& variablesContainer =
scope == VARIABLE_OR_PROPERTY_OR_PARAMETER
? variablesContainersList.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
? variablesContainersList
.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
: scope == VARIABLE_OR_PROPERTY
? variablesContainersList
.GetVariablesContainerFromVariableOrPropertyName(
variableName)
: variablesContainersList
.GetVariablesContainerFromVariableNameOnly(
variableName);
.GetVariablesContainerFromVariableOrPropertyName(variableName)
: variablesContainersList.GetVariablesContainerFromVariableNameOnly(
variableName);
const auto sourceType = variablesContainer.GetSourceType();
if (sourceType == gd::VariablesContainer::SourceType::Scene) {
variables = &variablesContainer;
@@ -1397,48 +1438,50 @@ gd::String EventsCodeGenerator::GenerateGetVariable(
gd::VariablesContainer::SourceType::ExtensionScene) {
variables = &variablesContainer;
output = "eventsFunctionContext.sceneVariablesForExtension";
} else if (sourceType ==
gd::VariablesContainer::SourceType::Properties) {
} else if (sourceType == gd::VariablesContainer::SourceType::Properties) {
if (hasChild) {
// Properties with children are not supported.
return "gdjs.VariablesContainer.badVariablesContainer";
}
const auto &propertiesContainersList =
const auto& propertiesContainersList =
GetProjectScopedContainers().GetPropertiesContainersList();
const auto &propertiesContainerAndProperty =
const auto& propertiesContainerAndProperty =
propertiesContainersList.Get(variableName);
return GeneratePropertyGetterWithoutCasting(
propertiesContainerAndProperty.first,
propertiesContainerAndProperty.second);
} else if (sourceType ==
gd::VariablesContainer::SourceType::Parameters) {
} else if (sourceType == gd::VariablesContainer::SourceType::Parameters) {
if (hasChild) {
// Parameters with children are not supported.
return "gdjs.VariablesContainer.badVariablesContainer";
}
const auto &parametersVectorsList =
const auto& parametersVectorsList =
GetProjectScopedContainers().GetParametersVectorsList();
const auto &parameter =
const auto& parameter =
gd::ParameterMetadataTools::Get(parametersVectorsList, variableName);
return GenerateParameterGetterWithoutCasting(parameter);
}
} else if (scope == LAYOUT_VARIABLE) {
output = "runtimeScene.getScene().getVariables()";
const auto *legacySceneVariables = GetProjectScopedContainers().GetLegacySceneVariables();
const auto* legacySceneVariables =
GetProjectScopedContainers().GetLegacySceneVariables();
if (HasProjectAndLayout()) {
variables = &GetLayout().GetVariables();
} else if (legacySceneVariables && legacySceneVariables->Has(variableName)) {
} else if (legacySceneVariables &&
legacySceneVariables->Has(variableName)) {
variables = legacySceneVariables;
output = "eventsFunctionContext.sceneVariablesForExtension";
}
} else if (scope == PROJECT_VARIABLE) {
output = "runtimeScene.getGame().getVariables()";
const auto *legacyGlobalVariables = GetProjectScopedContainers().GetLegacyGlobalVariables();
const auto* legacyGlobalVariables =
GetProjectScopedContainers().GetLegacyGlobalVariables();
if (HasProjectAndLayout()) {
variables = &GetProject().GetVariables();
} else if (legacyGlobalVariables && legacyGlobalVariables->Has(variableName)) {
} else if (legacyGlobalVariables &&
legacyGlobalVariables->Has(variableName)) {
variables = legacyGlobalVariables;
output = "eventsFunctionContext.globalVariablesForExtension";
}
@@ -1530,9 +1573,9 @@ gd::String EventsCodeGenerator::GenerateProfilerSectionEnd(
}
gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) {
bool isLocalProperty =
projectScopedContainers.GetPropertiesContainersList()
.GetBottomMostPropertiesContainer() == &propertiesContainer;
@@ -1558,8 +1601,8 @@ gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
}
gd::String EventsCodeGenerator::GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) {
bool isLocalProperty =
projectScopedContainers.GetPropertiesContainersList()
.GetBottomMostPropertiesContainer() == &propertiesContainer;
@@ -1626,7 +1669,7 @@ gd::String EventsCodeGenerator::GeneratePropertyGetter(
}
gd::String EventsCodeGenerator::GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) {
const gd::ParameterMetadata& parameter) {
return "eventsFunctionContext.getArgument(" +
ConvertToStringExplicit(parameter.GetName()) + ")";
}

View File

@@ -59,7 +59,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* Generate JavaScript for executing events of an events based function.
*
* \param project Project used.
* \param eventsFunctionsExtension The container of the compiled event function.
* \param eventsFunctionsExtension The container of the compiled event
* function.
* \param eventsFunction The events function to be compiled.
* \param codeNamespace Where to store the context used by the function.
* \param includeFiles Will be filled with the necessary include files.
@@ -96,7 +97,7 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
*/
static gd::String GenerateBehaviorEventsFunctionCode(
gd::Project& project,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedBehavior& eventsBasedBehavior,
const gd::EventsFunction& eventsFunction,
const gd::String& codeNamespace,
@@ -111,7 +112,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* function.
*
* \param project Project used.
* \param eventsBasedObject The object that contains the function to be compiled.
* \param eventsBasedObject The object that contains the function to be
* compiled.
* \param eventsFunction The events function to be compiled.
* \param codeNamespace Where to store the context used by the function.
* \param fullyQualifiedFunctionName The function name with its namespace.
@@ -129,7 +131,7 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
*/
static gd::String GenerateObjectEventsFunctionCode(
gd::Project& project,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedObject& eventsBasedObject,
const gd::EventsFunction& eventsFunction,
const gd::String& codeNamespace,
@@ -152,7 +154,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* \return Code
*/
virtual gd::String GenerateEventsListCode(
gd::EventsList& events, gd::EventsCodeGenerationContext& context) override;
gd::EventsList& events,
gd::EventsCodeGenerationContext& context) override;
/**
* Generate code for executing a condition list
@@ -194,7 +197,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* \brief Get the full name for accessing to a list of objects
*/
virtual gd::String GetObjectListName(
const gd::String& name, const gd::EventsCodeGenerationContext& context) override;
const gd::String& name,
const gd::EventsCodeGenerationContext& context) override;
/**
* \brief Get the namespace to be used to store code generated
@@ -226,9 +230,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
};
virtual gd::String GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) override;
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) override;
protected:
virtual gd::String GenerateParameterCodes(
@@ -289,7 +293,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "") override;
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "") override;
virtual gd::String GenerateBehaviorAction(
const gd::String& objectName,
@@ -299,7 +304,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "") override;
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "") override;
virtual gd::String GenerateGetBehaviorNameCode(
const gd::String& behaviorName) override;
@@ -320,9 +326,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
gd::String expressionCode) override {
// This uses `getChild` which allows to access a child
// with a number (an index, for an array) or a string (for a structure).
// This could be optimised, if the type of the accessed variable AND the type of the index is known,
// so that `getChildAt` (for an array, with an index) or `getChildNamed` (for a structure, with a name)
// is used instead.
// This could be optimised, if the type of the accessed variable AND the
// type of the index is known, so that `getChildAt` (for an array, with an
// index) or `getChildNamed` (for a structure, with a name) is used instead.
return ".getChild(" + expressionCode + ")";
};
@@ -330,29 +336,33 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
return "gdjs.VariablesContainer.badVariable";
}
virtual gd::String GeneratePropertyGetter(const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GeneratePropertyGetter(
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) override;
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) override;
virtual gd::String GenerateParameterGetter(const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) override;
const gd::ParameterMetadata& parameter) override;
virtual gd::String GenerateBadObject() override { return "null"; }
virtual gd::String GenerateObject(const gd::String& objectName,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateObject(
const gd::String& objectName,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateNegatedPredicate(const gd::String& predicate) const override {
virtual gd::String GenerateNegatedPredicate(
const gd::String& predicate) const override {
return "!(" + predicate + ")";
};
@@ -362,13 +372,15 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
virtual gd::String GenerateAllInstancesGetterCode(
const gd::String& objectName, gd::EventsCodeGenerationContext& context);
virtual gd::String GenerateProfilerSectionBegin(const gd::String& section) override;
virtual gd::String GenerateProfilerSectionEnd(const gd::String& section) override;
virtual gd::String GenerateProfilerSectionBegin(
const gd::String& section) override;
virtual gd::String GenerateProfilerSectionEnd(
const gd::String& section) override;
virtual gd::String GenerateRelationalOperation(
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) override;
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) override;
private:
static gd::String GenerateEventsListCompleteFunctionCode(
@@ -415,8 +427,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateFreeEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable);
/**
@@ -425,9 +437,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateBehaviorEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedBehavior& eventsBasedBehavior,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
const gd::String& thisObjectName,
const gd::String& thisBehaviorName);
@@ -438,9 +450,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateObjectEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedObject& eventsBasedObject,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
const gd::String& thisObjectName);
@@ -455,7 +467,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
/**
* \brief Construct a code generator for the specified containers.
*/
EventsCodeGenerator(const gd::ProjectScopedContainers& projectScopedContainers);
EventsCodeGenerator(
const gd::ProjectScopedContainers& projectScopedContainers);
virtual ~EventsCodeGenerator();
gd::String codeNamespace; ///< Optional namespace for the generated code,
@@ -467,16 +480,16 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* to provides access objects, object creation and access to arguments from
* the rest of the events.
*/
gd::String GenerateEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsContainer &eventsFunctionsContainer,
const gd::EventsFunction &eventsFunction,
const gd::String &onceTriggersVariable,
gd::String &objectsGettersMap,
gd::String &objectArraysMap,
gd::String &behaviorNamesMap,
const gd::String &thisObjectName = "",
const gd::String &thisBehaviorName = "");
gd::String GenerateEventsFunctionContext(
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsContainer& eventsFunctionsContainer,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
gd::String& objectsGettersMap,
gd::String& objectArraysMap,
gd::String& behaviorNamesMap,
const gd::String& thisObjectName = "",
const gd::String& thisBehaviorName = "");
};
} // namespace gdjs

View File

@@ -22,9 +22,11 @@ AsyncExtension::AsyncExtension() {
gd::AsyncEvent &event = dynamic_cast<gd::AsyncEvent &>(event_);
// Generate callback code
const auto callbackDescriptor = codeGenerator.GenerateCallback(
const auto callbackId =
gd::String::From(codeGenerator.GenerateSingleUsageUniqueIdFor(
event.GetInstruction().GetOriginalInstruction().lock().get())),
event.GetInstruction().GetOriginalInstruction().lock().get()));
const auto callbackDescriptor = codeGenerator.GenerateCallback(
callbackId,
parentContext,
event.GetActions(),
event.HasSubEvents() ? &event.GetSubEvents() : nullptr);
@@ -33,10 +35,6 @@ AsyncExtension::AsyncExtension() {
"(runtimeScene) => (" + callbackDescriptor.functionName + "(" +
callbackDescriptor.argumentsList + "))";
// Generate the action and store the generated task.
const gd::String asyncActionCode = codeGenerator.GenerateActionCode(
event.GetInstruction(), parentContext, callbackCallCode);
// Generate code to backup the objects lists.
// Do it after generating the code of the action so that it uses the
// same object list as used in the action.
@@ -70,6 +68,13 @@ AsyncExtension::AsyncExtension() {
", obj);\n";
}
// Generate the action and store the generated task.
const gd::String asyncActionCode =
codeGenerator.GenerateActionCode(event.GetInstruction(),
parentContext,
callbackCallCode,
callbackId);
return "{\n" + parentAsyncObjectsListGetter + "{\n" +
asyncContextBuilder + asyncActionCode + "}\n" + "}\n";
});

View File

@@ -53,6 +53,7 @@ BaseObjectExtension::BaseObjectExtension() {
"runtimeobject.js");
objectActions["RotateTowardAngle"].SetFunctionName("rotateTowardAngle");
objectActions["RotateTowardPosition"].SetFunctionName("rotateTowardPosition");
objectActions["RotateTowardObject"].SetFunctionName("rotateTowardObject");
objectActions["ChangeLayer"].SetFunctionName("setLayer");
objectConditions["Layer"].SetFunctionName("isOnLayer");
objectActions["ChangePlan"]
@@ -229,6 +230,8 @@ BaseObjectExtension::BaseObjectExtension() {
"gdjs.evtTools.object.pickRandomObject");
GetAllConditions()["AjoutHasard"].SetFunctionName(
"gdjs.evtTools.object.pickRandomObject");
GetAllActions()["PickNearest"].SetFunctionName(
"gdjs.evtTools.object.pickNearestObject");
GetAllConditions()["PickNearest"].SetFunctionName(
"gdjs.evtTools.object.pickNearestObject");

View File

@@ -841,13 +841,23 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
event.GetParameterObjects(),
parentContext.GetCurrentObject());
callingCode += "var objects = [];\n";
for (unsigned int i = 0; i < realObjects.size(); ++i) {
parentContext.ObjectsListNeeded(realObjects[i]);
if (realObjects.size() == 1) {
parentContext.ObjectsListNeeded(realObjects[0]);
callingCode +=
"objects.push.apply(objects," +
codeGenerator.GetObjectListName(realObjects[i], parentContext) +
");\n";
"const objects = " +
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
";\n";
} else {
// Groups are rarely used in JS events so it's fine to make
// allocations.
callingCode += "const objects = [];\n";
for (unsigned int i = 0; i < realObjects.size(); ++i) {
parentContext.ObjectsListNeeded(realObjects[i]);
callingCode += "objects.push.apply(objects," +
codeGenerator.GetObjectListName(realObjects[i],
parentContext) +
");\n";
}
}
}

View File

@@ -18,6 +18,8 @@ KeyboardExtension::KeyboardExtension() {
"gdjs.evtTools.input.wasKeyReleased");
GetAllConditions()["KeyFromTextPressed"].SetFunctionName(
"gdjs.evtTools.input.isKeyPressed");
GetAllConditions()["KeyFromTextJustPressed"].SetFunctionName(
"gdjs.evtTools.input.wasKeyJustPressed");
GetAllConditions()["KeyFromTextReleased"].SetFunctionName(
"gdjs.evtTools.input.wasKeyReleased");
GetAllConditions()["AnyKeyPressed"].SetFunctionName(

View File

@@ -844,6 +844,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "CustomRuntimeObjectInstanceContainer.js");
InsertUnique(includesFiles, "CustomRuntimeObject.js");
InsertUnique(includesFiles, "CustomRuntimeObject2D.js");
InsertUnique(includesFiles, "indexeddb.js");
// Common includes for events only.
InsertUnique(includesFiles, "events-tools/commontools.js");

View File

@@ -4,6 +4,33 @@
* This project is released under the MIT License.
*/
namespace gdjs {
export type WaitTaskNetworkSyncData = {
type: 'wait';
duration: float;
timeElapsedOnScene: float;
};
export type ResolveTaskNetworkSyncData = null;
export type PromiseTaskNetworkSyncData = null;
export type ManuallyResolvableTaskNetworkSyncData = null;
export type TaskGroupNetworkSyncData = {
type: 'group';
tasks: AsyncTaskNetworkSyncData[];
};
export type AsyncTaskNetworkSyncData =
| WaitTaskNetworkSyncData
| TaskGroupNetworkSyncData
| PromiseTaskNetworkSyncData
| ManuallyResolvableTaskNetworkSyncData
| ResolveTaskNetworkSyncData;
export type AsyncTasksManagerNetworkSyncData = {
tasks: Array<{
callbackId: string;
asyncTask: AsyncTaskNetworkSyncData;
objectsList: gdjs.LongLivedObjectsListNetworkSyncData;
}>;
};
/**
* This stores all asynchronous tasks waiting to be completed,
* for a given scene.
@@ -15,7 +42,12 @@ namespace gdjs {
*/
private tasksWithCallback = new Array<{
asyncTask: AsyncTask;
callback: (runtimeScene: gdjs.RuntimeScene) => void;
callback: (
runtimeScene: gdjs.RuntimeScene,
longLivedObjectsList: gdjs.LongLivedObjectsList
) => void;
callbackId: string;
longLivedObjectsList: gdjs.LongLivedObjectsList;
}>();
/**
@@ -26,7 +58,10 @@ namespace gdjs {
const taskWithCallback = this.tasksWithCallback[i];
if (taskWithCallback.asyncTask.update(runtimeScene)) {
// The task has finished, run the callback and remove it.
taskWithCallback.callback(runtimeScene);
taskWithCallback.callback(
runtimeScene,
taskWithCallback.longLivedObjectsList
);
this.tasksWithCallback.splice(i--, 1);
}
}
@@ -39,9 +74,19 @@ namespace gdjs {
*/
addTask(
asyncTask: AsyncTask,
callback: (runtimeScene: RuntimeScene) => void
callback: (
runtimeScene: RuntimeScene,
longLivedObjectsList: gdjs.LongLivedObjectsList
) => void,
callbackId: string,
longLivedObjectsList: gdjs.LongLivedObjectsList
): void {
this.tasksWithCallback.push({ asyncTask, callback });
this.tasksWithCallback.push({
asyncTask,
callback,
callbackId,
longLivedObjectsList,
});
}
/**
@@ -51,6 +96,80 @@ namespace gdjs {
clearTasks() {
this.tasksWithCallback.length = 0;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): AsyncTasksManagerNetworkSyncData {
const tasksData = this.tasksWithCallback
.map(({ asyncTask, callbackId, longLivedObjectsList }) => {
// Stay compatible with old extensions declaring tasks without a getNetworkSyncData method.
if (!asyncTask.getNetworkSyncData) return null;
return {
callbackId,
asyncTask: asyncTask.getNetworkSyncData(),
objectsList: longLivedObjectsList.getNetworkSyncData(syncOptions),
};
})
.filter(Boolean);
return {
tasks: tasksData,
};
}
updateFromNetworkSyncData(
syncData: AsyncTasksManagerNetworkSyncData,
idToCallbackMap: Map<
string,
(
runtimeScene: gdjs.RuntimeScene,
asyncObjectsList: gdjs.LongLivedObjectsList
) => void
>,
runtimeScene: gdjs.RuntimeScene,
syncOptions: UpdateFromNetworkSyncDataOptions
) {
this.clearTasks();
const unknownTaskTypes: string[] = [];
syncData.tasks.forEach(({ callbackId, asyncTask, objectsList }) => {
if (!asyncTask) return;
const callback = idToCallbackMap.get(callbackId);
if (callback) {
const longLivedObjectsList = new gdjs.LongLivedObjectsList();
longLivedObjectsList.updateFromNetworkSyncData(
objectsList,
runtimeScene,
syncOptions
);
if (asyncTask.type === 'group') {
const task = new TaskGroup();
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task, callback, callbackId, longLivedObjectsList);
} else if (asyncTask.type === 'wait') {
const task = new gdjs.evtTools.runtimeScene.WaitTask(
asyncTask.duration
);
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task, callback, callbackId, longLivedObjectsList);
} else {
// Unknown task type.
// @ts-ignore
unknownTaskTypes.push(asyncTask.type);
}
}
});
if (unknownTaskTypes.length) {
console.warn(
`${unknownTaskTypes.length} asynchronous task(s) could not be restored from network sync data. ${unknownTaskTypes.join(', ')}`
);
}
}
}
/**
@@ -63,6 +182,12 @@ namespace gdjs {
* @return True if the task is finished, false if it needs to continue running.
*/
abstract update(runtimeScene: RuntimeScene): boolean;
abstract getNetworkSyncData(): AsyncTaskNetworkSyncData;
abstract updateFromNetworkSyncData(
syncData: AsyncTaskNetworkSyncData
): void;
}
export class TaskGroup extends AsyncTask {
@@ -80,12 +205,54 @@ namespace gdjs {
return this.tasks.length === 0;
}
getNetworkSyncData(): TaskGroupNetworkSyncData {
return {
type: 'group',
tasks: this.tasks.map((task) => task.getNetworkSyncData()),
};
}
updateFromNetworkSyncData(syncData: TaskGroupNetworkSyncData) {
const unknownTaskTypes: string[] = [];
syncData.tasks.forEach((asyncTask) => {
if (!asyncTask) return;
if (asyncTask.type === 'group') {
const task = new TaskGroup();
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task);
} else if (asyncTask.type === 'wait') {
const task = new gdjs.evtTools.runtimeScene.WaitTask(
asyncTask.duration
);
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task);
} else {
// Unknown task type.
// @ts-ignore
unknownTaskTypes.push(asyncTask.type);
}
});
if (unknownTaskTypes.length) {
console.warn(
`${unknownTaskTypes.length} asynchronous task(s) could not be restored from network sync data. ${unknownTaskTypes.join(', ')}`
);
}
}
}
export class ResolveTask extends AsyncTask {
update() {
return true;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
const logger = new gdjs.Logger('Internal PromiseTask');
@@ -121,6 +288,12 @@ ${error ? 'The following error was thrown: ' + error : ''}`
update() {
return this.isResolved;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
export class ManuallyResolvableTask extends AsyncTask {
@@ -133,5 +306,11 @@ ${error ? 'The following error was thrown: ' + error : ''}`
update(): boolean {
return this.isResolved;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
}

View File

@@ -5,7 +5,7 @@
<name>GDJS_PROJECTNAME</name>
<content src="index.html" />
<plugin name="cordova-plugin-whitelist" version="1" />
<plugin name="cordova-plugin-screen-orientation" version="3.0.2" />
<plugin name="cordova-plugin-screen-orientation" version="3.0.4" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
@@ -67,4 +67,4 @@
<!-- Keep cordova-plugin-ionic-webview plugin last as it has a deployment-target to 11, which
affects the installation of other plugins.-->
<plugin name="cordova-plugin-ionic-webview" version="5.0.1" />
</widget>
</widget>

View File

@@ -17,11 +17,19 @@ namespace gdjs {
isInnerAreaFollowingParentSize: boolean;
};
export type CustomObjectNetworkSyncDataType = ObjectNetworkSyncData & {
export type CustomObjectNetworkSyncDataType = {
anim?: SpriteAnimatorNetworkSyncData;
ifx: boolean;
ify: boolean;
sx: float;
sy: float;
op: float;
cc?: [float, float];
};
export type CustomObjectNetworkSyncData = ObjectNetworkSyncData &
CustomObjectNetworkSyncDataType;
/**
* An object that contains other object.
*
@@ -221,24 +229,66 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): CustomObjectNetworkSyncDataType {
return {
...super.getNetworkSyncData(),
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): CustomObjectNetworkSyncData {
const animator = this.getAnimator();
const networkSyncData: CustomObjectNetworkSyncData = {
...super.getNetworkSyncData(syncOptions),
ifx: this.isFlippedX(),
ify: this.isFlippedY(),
sx: this._scaleX,
sy: this._scaleY,
op: this.opacity,
};
if (animator) {
networkSyncData.anim = animator.getNetworkSyncData();
}
if (this._customCenter) {
networkSyncData.cc = this._customCenter;
}
return networkSyncData;
}
updateFromNetworkSyncData(
networkSyncData: CustomObjectNetworkSyncDataType
networkSyncData: CustomObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.ifx !== undefined) {
this.flipX(networkSyncData.ifx);
}
if (networkSyncData.ify !== undefined) {
this.flipY(networkSyncData.ify);
}
if (networkSyncData.sx !== undefined) {
this.setScaleX(Math.abs(networkSyncData.sx));
}
if (networkSyncData.sy !== undefined) {
this.setScaleY(Math.abs(networkSyncData.sy));
}
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
}
if (networkSyncData.anim) {
const animator = this.getAnimator();
if (animator) {
animator.updateFromNetworkSyncData(networkSyncData.anim);
}
}
if (networkSyncData.cc) {
this.setRotationCenter(networkSyncData.cc[0], networkSyncData.cc[1]);
}
if (
networkSyncData.ifx !== undefined ||
networkSyncData.ify !== undefined ||
networkSyncData.sx !== undefined ||
networkSyncData.sy !== undefined ||
networkSyncData.anim !== undefined ||
networkSyncData.cc !== undefined
) {
this.onChildrenLocationChanged();
}
}
override extraInitializationFromInitialInstance(
@@ -300,8 +350,13 @@ namespace gdjs {
if (profiler) {
profiler.end(this.type);
}
}
this._instanceContainer._updateObjectsPostEvents();
override stepBehaviorsPostEvents(
instanceContainer: gdjs.RuntimeInstanceContainer
): void {
super.stepBehaviorsPostEvents(instanceContainer);
this._instanceContainer._stepBehaviorsPostEvents();
}
/**

View File

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

View File

@@ -534,7 +534,9 @@ namespace gdjs {
`Unloading of resources of kind ${kindResourceManager} for scene ${unloadedSceneName}: `,
resources.map((resource) => resource.name).join(', ')
);
resourceManager.unloadResourcesList(resources);
for (const resource of resources) {
resourceManager.unloadResource(resource);
}
}
}

View File

@@ -31,19 +31,19 @@ namespace gdjs {
getResourceKinds(): Array<ResourceKind>;
/**
* Should clear all resources, data, loaders stored by this manager.
* Clear all resources, data, loaders stored by this manager.
* Using the manager after calling this method is undefined behavior.
*/
dispose(): void;
/**
* Should clear all specified resources data and anything stored by this manager
* for these resources.
* Clear any data in cache for a resource. Embedded resources are also
* cleared.
*
* Usually called when scene resoures are unloaded.
* Usually called when scene resources are unloaded.
*
* @param resourcesList The list of specific resources that need to be clear
* @param resourceData The resource to clear
*/
unloadResourcesList(resourcesList: ResourceData[]): void;
unloadResource(resourceData: ResourceData): void;
}
}

View File

@@ -312,15 +312,15 @@ namespace gdjs {
xPos: float,
yPos: float,
zPos: float,
trackByPersistentUuid: boolean
trackByPersistentUuid: boolean,
options?: {
excludedObjectNames?: Set<string> | null;
}
): void {
let zOffset: number;
let shouldTrackByPersistentUuid: boolean;
let zOffset: number = zPos;
let shouldTrackByPersistentUuid: boolean = trackByPersistentUuid;
if (arguments.length === 5) {
zOffset = zPos;
shouldTrackByPersistentUuid = trackByPersistentUuid;
} else {
if (arguments.length <= 4) {
/**
* Support for the previous signature (before 3D was introduced):
* createObjectsFrom(data, xPos, yPos, trackByPersistentUuid)
@@ -332,6 +332,10 @@ namespace gdjs {
for (let i = 0, len = data.length; i < len; ++i) {
const instanceData = data[i];
const objectName = instanceData.name;
if (options?.excludedObjectNames?.has(objectName)) {
continue;
}
const newObject = this.createObject(objectName);
if (newObject !== null) {
if (shouldTrackByPersistentUuid) {
@@ -576,9 +580,9 @@ namespace gdjs {
}
/**
* Update the objects (update positions, time management...)
* Call each behavior stepPostEvents method.
*/
_updateObjectsPostEvents() {
_stepBehaviorsPostEvents() {
this._cacheOrClearRemovedInstances();
// It is *mandatory* to create and iterate on a external list of all objects, as the behaviors
@@ -609,7 +613,7 @@ namespace gdjs {
* @param name Name of the object for which the instances must be returned.
* @return The list of objects with the given name
*/
getObjects(name: string): gdjs.RuntimeObject[] | undefined {
getObjects(name: string): gdjs.RuntimeObject[] {
if (!this._instances.containsKey(name)) {
logger.info(
'RuntimeScene.getObjects: No instances called "' +
@@ -623,7 +627,7 @@ namespace gdjs {
/**
* Create a new object from its name. The object is also added to the instances
* living in the container (No need to call {@link gdjs.RuntimeScene.addObject})
* living in the container (No need to call {@link addObject})
* @param objectName The name of the object to be created
* @return The created object
*/
@@ -636,18 +640,20 @@ namespace gdjs {
return null;
}
const objectData = this._objects.get(objectName);
// Create a new object using the object constructor (cached during loading)
// and the stored object's data:
const cache = this._instancesCache.get(objectName);
const ctor = this._objectsCtor.get(objectName);
let obj;
if (!cache || cache.length === 0) {
obj = new ctor(this, this._objects.get(objectName));
obj = new ctor(this, objectData);
} else {
// Reuse an objet destroyed before. If there is an object in the cache,
// then it means it does support reinitialization.
obj = cache.pop();
obj.reinitialize(this._objects.get(objectName));
obj.reinitialize(objectData);
}
this.addObject(obj);
return obj;

View File

@@ -116,6 +116,101 @@ namespace gdjs {
}
}
getNetworkSyncData(): LayerNetworkSyncData {
const effectsNetworkSyncData = {};
for (const effectName in this._rendererEffects) {
effectsNetworkSyncData[effectName] =
this._rendererEffects[effectName].getNetworkSyncData();
}
return {
timeScale: this._timeScale,
defaultZOrder: this._defaultZOrder,
hidden: this._hidden,
effects: effectsNetworkSyncData,
followBaseLayerCamera: this._followBaseLayerCamera,
clearColor: this._clearColor,
cameraX: this.getCameraX(),
cameraY: this.getCameraY(),
cameraZ: this.getCameraZ(null),
cameraRotation: this.getCameraRotation(),
cameraZoom: this.getCameraZoom(),
};
}
updateFromNetworkSyncData(networkSyncData: LayerNetworkSyncData): void {
if (
networkSyncData.timeScale !== undefined &&
networkSyncData.timeScale !== this._timeScale
) {
this.setTimeScale(networkSyncData.timeScale);
}
if (
networkSyncData.defaultZOrder !== undefined &&
networkSyncData.defaultZOrder !== this._defaultZOrder
) {
this.setDefaultZOrder(networkSyncData.defaultZOrder);
}
if (
this._hidden != undefined &&
this._hidden !== networkSyncData.hidden
) {
this.show(!networkSyncData.hidden);
}
if (
networkSyncData.followBaseLayerCamera !== undefined &&
networkSyncData.followBaseLayerCamera !== this._followBaseLayerCamera
) {
this.setFollowBaseLayerCamera(networkSyncData.followBaseLayerCamera);
}
if (
networkSyncData.clearColor !== undefined &&
networkSyncData.clearColor !== this._clearColor
) {
this.setClearColor(
networkSyncData.clearColor[0] * 255,
networkSyncData.clearColor[1] * 255,
networkSyncData.clearColor[2] * 255
);
}
if (
networkSyncData.cameraX !== undefined &&
networkSyncData.cameraX !== this.getCameraX()
) {
this.setCameraX(networkSyncData.cameraX);
}
if (
networkSyncData.cameraY !== undefined &&
networkSyncData.cameraY !== this.getCameraY()
) {
this.setCameraY(networkSyncData.cameraY);
}
if (
networkSyncData.cameraZ !== undefined &&
networkSyncData.cameraZ !== this.getCameraZ(null)
) {
this.setCameraZ(networkSyncData.cameraZ, null);
}
if (
networkSyncData.cameraRotation !== undefined &&
networkSyncData.cameraRotation !== this.getCameraRotation()
) {
this.setCameraRotation(networkSyncData.cameraRotation);
}
if (
networkSyncData.cameraZoom !== undefined &&
networkSyncData.cameraZoom !== this.getCameraZoom()
) {
this.setCameraZoom(networkSyncData.cameraZoom);
}
for (const effectName in networkSyncData.effects) {
this._rendererEffects[effectName].updateFromNetworkSyncData(
networkSyncData.effects[effectName]
);
}
}
getRuntimeLayer(): gdjs.RuntimeLayer {
return this;
}

View File

@@ -348,7 +348,7 @@ namespace gdjs {
// It can't actually happen.
this._logs.push({
kind: 'error',
message: "Can't hot-reload as no scene are opened.",
message: "Can't hot-reload as no scene is opened.",
});
return;
}

View File

@@ -153,7 +153,7 @@ namespace gdjs {
};
/**
* Return true if the specified key is pressed
* Return true if the specified key is pressed (i.e: just pressed or held down).
*
*/
export const isKeyPressed = function (
@@ -170,8 +170,22 @@ namespace gdjs {
};
/**
* Return true if the specified key was just released
*
* Return true if the specified key was just pressed (i.e: it started being pressed
* during this frame).
*/
export const wasKeyJustPressed = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
key: string
) {
return instanceContainer
.getGame()
.getInputManager()
.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode[key]);
};
/**
* Return true if the specified key was just released (i.e: it stopped being pressed
* during this frame).
*/
export const wasKeyReleased = function (
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -187,7 +201,7 @@ namespace gdjs {
};
/**
* Return the name of the last key pressed in the game
* Return the name of the last key pressed in the game.
*/
export const lastPressedKey = function (
instanceContainer: gdjs.RuntimeInstanceContainer

View File

@@ -410,7 +410,12 @@ namespace gdjs {
return true;
};
export const pickNearestObject = function (objectsLists, x, y, inverted) {
export const pickNearestObject = function (
objectsLists: ObjectsLists,
x: float,
y: float,
inverted?: boolean | undefined
) {
let bestObject = null;
let best = 0;
let first = true;
@@ -421,8 +426,7 @@ namespace gdjs {
for (let j = 0; j < list.length; ++j) {
const object = list[j];
const distance = object.getSqDistanceToPosition(x, y);
// @ts-ignore
if (first || (distance < best) ^ inverted) {
if (first || distance < best !== inverted) {
best = distance;
bestObject = object;
}
@@ -622,6 +626,14 @@ namespace gdjs {
}
}
const logger = new gdjs.Logger('LongLivedObjectsLists');
export type LongLivedObjectsListNetworkSyncData = {
objectsLists: {
[objectName: string]: Array<string>;
};
localVariablesContainers: Array<Array<VariableNetworkSyncData>>;
};
/**
* A container for objects lists that should last more than the current frame.
* It automatically removes objects that were destroyed from the objects lists.
@@ -695,5 +707,85 @@ namespace gdjs {
): void {
gdjs.copyArray(variablesContainers, this.localVariablesContainers);
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): LongLivedObjectsListNetworkSyncData {
const objectsLists: {
[objectName: string]: Array<string>;
} = {};
for (const [objectName, runtimeObjects] of this.objectsLists.entries()) {
const objectNetworkIds: Array<string> = [];
for (const runtimeObject of runtimeObjects) {
const objectNetworkId = runtimeObject.getNetworkId();
if (!objectNetworkId) {
logger.warn(
'Tried to get sync data of a LongLivedObjectsList and found an object without a network ID'
);
continue;
}
objectNetworkIds.push(objectNetworkId);
}
objectsLists[objectName] = objectNetworkIds;
}
return {
objectsLists,
localVariablesContainers: this.localVariablesContainers.map(
(container) => container.getNetworkSyncData(syncOptions)
),
};
}
updateFromNetworkSyncData(
syncData: LongLivedObjectsListNetworkSyncData,
runtimeScene: gdjs.RuntimeScene,
syncOptions: UpdateFromNetworkSyncDataOptions
) {
const { objectsLists, localVariablesContainers } = syncData;
// Clear the current state.
this.objectsLists.clear();
this.localVariablesContainers.length = 0;
// Restore the list of objects.
for (const [objectName, objectNetworkIds] of Object.entries(
objectsLists
)) {
const runtimeObjects = runtimeScene.getObjects(objectName);
if (!runtimeObjects) {
logger.warn(
'Tried to update sync data of a LongLivedObjectsList but cannot find objects with name: ' +
objectName
);
continue;
}
const runtimeObjectsFromSyncData = runtimeObjects.filter(
(runtimeObject) => {
const runtimeObjectNetworkId = runtimeObject.getNetworkId();
return (
!!runtimeObjectNetworkId &&
objectNetworkIds.includes(runtimeObjectNetworkId)
);
}
);
for (const runtimeObject of runtimeObjectsFromSyncData) {
this.addObject(objectName, runtimeObject);
}
}
// Restore the local variables containers.
this.localVariablesContainers = localVariablesContainers.map(
(localVariablesContainer) => {
const newContainer = new gdjs.VariablesContainer();
newContainer.updateFromNetworkSyncData(
localVariablesContainer,
syncOptions
);
return newContainer;
}
);
}
}
}

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