Compare commits

...

79 Commits

Author SHA1 Message Date
Clément Pasteau
7d161d7b8f wip 2024-07-05 11:15:20 +02:00
D8H
98befc8000 Fix group list updating. (#6748)
Do not show in changelog
2024-07-03 10:57:59 +02:00
Clément Pasteau
377231fb37 Bump to 5.4.205 (#6746) 2024-07-03 10:27:36 +02:00
github-actions[bot]
22ae8ac489 Update translations [skip ci] (#6744)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-03 10:27:24 +02:00
Clément Pasteau
40674fd3b9 Guided lessons opening fixes (#6745)
Do not show in changelog
2024-07-02 17:40:46 +02:00
Clément Pasteau
0792e59b24 Speed up lobbies process in preview (#6743)
- start countdown is reduced
- players are readied automatically
2024-07-02 16:12:51 +02:00
github-actions[bot]
8ba6ad7b43 Update translations [skip ci] (#6741)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-02 15:48:29 +02:00
Clément Pasteau
84b07bc84d Open all scenes and focus on first one when opening a guided lesson (#6740)
Do not show in changelog
2024-07-02 15:48:09 +02:00
Clément Pasteau
c46d39cbed Fix returning player auth on first preview (#6742)
Do not show in changelog
2024-07-02 15:21:48 +02:00
github-actions[bot]
b96132964c Update translations [skip ci] (#6734)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-02 14:13:22 +02:00
Tristan Rhodes
130cd1e3a7 "Change platform type" action now uses a dropdown selector (#6727) 2024-07-02 14:11:39 +02:00
Clément Pasteau
fbbcf25bf5 Allow tagging a behavior of a multiplayer object as not synced (#6737)
* Also fix the synchronization of the activated state of a behavior
2024-07-02 14:05:51 +02:00
Aurélien Vivet
ffaae4d3d4 New guided lesson - Creating a Multiplayer Co-op game (#6736) 2024-07-02 10:35:36 +02:00
D8H
75d79a7758 Display errors on variable parameters about name collisions with objects (#6732) 2024-07-01 16:42:43 +02:00
D8H
d91fb78848 Use a new icon for global variables (#6735)
- Don't show in changelog
2024-07-01 15:15:41 +02:00
D8H
e0a2ed1654 Fix a refreshing issue of the object list in functions (#6725)
- Fix the default parameters when pasting a function in a behavior
2024-07-01 14:49:09 +02:00
Florian Rival
d5f2be1c19 Add an extra link to create a variable (#6730) 2024-07-01 14:28:51 +02:00
github-actions[bot]
7abcfe8af2 Update translations [skip ci] (#6731)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-07-01 13:01:08 +02:00
Clément Pasteau
b86dd9efce Bump memory for tests (#6733)
Do not show in changelog
2024-07-01 13:00:47 +02:00
github-actions[bot]
15f6b62c5b Update translations [skip ci] (#6724)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-07-01 12:04:26 +02:00
Clément Pasteau
eb995ec7c7 Fix emscripten core version (#6729)
Show in developer changelog only
2024-07-01 12:03:46 +02:00
D8H
a076571120 Fix some array variable expressions visibility (#6728) 2024-07-01 11:37:50 +02:00
Clément Pasteau
ec9cb790e7 Improve multiplayer messages by automatically detecting and using best compression method for lobby (#6690)
- Also remove dependency on P2P extension
2024-07-01 10:52:01 +02:00
D8H
4a283add00 Fix events shortcuts from triggering when a dialog is opened (#6726) 2024-06-30 17:31:00 +02:00
D8H
6b3faa42bb Make variables easier to declare on the fly (#6721) 2024-06-27 21:13:18 +02:00
Florian Rival
555ee61e63 Fix badges text color
Don't show in changelog
2024-06-27 13:00:33 +02:00
github-actions[bot]
b838c8549b Update translations [skip ci] (#6722)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-27 10:44:05 +02:00
Clément Pasteau
33db6ee359 Fix message sender expression (#6723)
Do not show in changelog
2024-06-27 10:43:42 +02:00
Clément Pasteau
f361d3e1fa Automatically log the user as a player in a preview (#6714)
* Simplifies creating & testing a game in preview, when using player authentication or multiplayer extensions
2024-06-27 09:38:23 +02:00
github-actions[bot]
441401f34c Update translations [skip ci] (#6719)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-06-26 16:09:55 +02:00
Aurélien Vivet
558daa2075 Remove extra "Add" words in the events context menu (#6713) 2024-06-26 09:37:50 +02:00
AlexandreS
cd475316df Limit leaderboard customization css field length (#6720) 2024-06-26 07:42:41 +02:00
D8H
8b21e72c85 Fix to avoid new variables from being added at the top (#6718) 2024-06-25 17:34:12 +02:00
github-actions[bot]
a9d6f18c11 Update translations [skip ci] (#6689)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-06-25 17:10:32 +02:00
Clément Pasteau
f58e1113b6 Add new actions and expressions for multiplayer (#6717)
* Send a variable with a custom message
* Retrieve the player number of the sender
2024-06-25 17:07:38 +02:00
D8H
f82b5fc66d Fix local variables default values when the wait action is used (#6715) 2024-06-25 16:14:40 +02:00
AlexandreS
2f19a9bb33 Improve some icons in the editor (variables, mobile toolbar) (#6700) 2024-06-25 12:42:41 +02:00
Dennis Fehr
8f739d85c2 Fix dragging bug in the Events Sheet when the indent scale is not 1x (#6702) 2024-06-25 10:07:09 +02:00
AlexandreS
f23847617d Add possibility for players to login with Google/Apple in game. (#6711) 2024-06-24 18:22:43 +02:00
Clément Pasteau
876332a782 Fix multiplayer lobbies properly opening after login (#6712) 2024-06-24 18:15:32 +02:00
D8H
93c74c9fd6 Use property names instead of property labels in descriptions and sentences of internal behavior instructions (#6708) 2024-06-23 17:45:21 +02:00
Florian Rival
e92d8496ac Add missing Spine Runtime license agreement and link to Spine website (#6701) 2024-06-21 18:18:03 +02:00
AlexandreS
35e67a6d26 Try to register game before copying lobby configuration (#6696)
Don't show in changelog
2024-06-20 18:04:29 +02:00
Clément Pasteau
740c7ae8bc Upgrade CircleCI macOS builders to m1 (#6688)
Only show in developer changelog
2024-06-20 10:26:47 +02:00
Clément Pasteau
432a91c47b Bump to 5.4.204 (#6682) 2024-06-20 08:40:39 +02:00
Clément Pasteau
799bc762aa Fix wrong import (#6687)
Do not show in changelog
2024-06-20 08:40:26 +02:00
github-actions[bot]
147a0eed53 Update translations [skip ci] (#6683)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-20 07:58:09 +02:00
Aurélien Vivet
920e1d423f rename tutorial (#6684)
Do not show in changelog
2024-06-19 20:41:05 +02:00
Aurélien Vivet
c013f319ee Add the new guided lesson: Fire Bullets with an action from a behavior (#6681) 2024-06-19 19:49:46 +02:00
Clément Pasteau
db53e84c6f Add more params to lobby connection (#6677)
Do not show in changelog
2024-06-19 19:02:13 +02:00
github-actions[bot]
4b85f710a9 Update translations [skip ci] (#6673)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-19 18:06:54 +02:00
Clément Pasteau
07a350dadd Use a separate logger for debugging multiplayer (#6680)
Do not show in changelog
2024-06-19 17:07:20 +02:00
Clément Pasteau
7ccbf91973 Automatically disable default controls for synchronized objects (#6678)
* For top down & platformer objects, when the object is not owned by the current player, it will automatically have default controls disabled
2024-06-18 18:19:56 +02:00
Clément Pasteau
410fecf715 Multiplayer improvements (#6676)
* An object changing layer during the game is now properly moved to the layer on the other players games
* Video object can be synced as well
* Pathfinding behavior is now properly synced, allowing a smooth prediction on other players games
* Spine object is now synced as well
2024-06-18 16:52:41 +02:00
AlexandreS
967bf5cbe3 Reinstate shadow behind snackbar (#6674)
Don't show in changelog
2024-06-18 15:37:32 +02:00
github-actions[bot]
0020f72850 Update translations [skip ci] (#6669)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-18 12:39:01 +02:00
Clément Pasteau
0eb7b85e77 Improve multiplayer sync (#6672)
* Fix a rare case where the lobby was joined but did not appear as such in the interface
* Text inputs are now synced properly
* Add 2 new expressions for current player ping & username
* Fix Physics2 behavior being properly synced
2024-06-18 12:38:16 +02:00
Florian Rival
063bf51783 Add 'reference' to generated documentation reference page titles [skip ci]
Don't show in changelog
2024-06-18 10:50:50 +02:00
Clément Pasteau
482fb3b85e Allow customizing the Events sheet indent scale (#6671)
Co-authored-by: Dennis Fehr <dennis.james.fehr@gmail.com> @cyraid
2024-06-18 10:49:39 +02:00
Florian Rival
9a4a84d2af Fix Player object ownership action/condition sentence 2024-06-17 21:56:50 +02:00
Florian Rival
9a705b98e2 Fix crash when launching multiple previews with a pop-up blocker 2024-06-17 18:37:50 +02:00
Clément Pasteau
da940abdc2 Multiplayer fixes (#6668)
* Prevent verticesBuffer to be synced for Physics2
* Ensure we create a force if no recycled are available
2024-06-17 18:35:20 +02:00
github-actions[bot]
528b8f4e6c Update translations [skip ci] (#6663)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-17 15:06:36 +02:00
Clément Pasteau
7212d56a1b Add folders for multiplayer extension (#6667)
Do not show in changelog
2024-06-17 14:53:12 +02:00
Clément Pasteau
5cba43335c Fix sentence for taking ownership (#6666)
Do not show in changelog
2024-06-17 14:28:08 +02:00
Clément Pasteau
324698e269 Fix parsing wrongly the variableNetworkId (#6665)
Do not show in changelog
2024-06-17 14:19:40 +02:00
Clément Pasteau
d219bf05d3 Fix the animation frame update for a Sprite when receiving new animation from the network (#6664) 2024-06-17 13:20:26 +02:00
D8H
9cbc421d74 Generate a default sentence for event-based functions (#6638) 2024-06-16 23:18:54 +02:00
Florian Rival
f63d9d1b5c Bump version 2024-06-16 10:30:32 +02:00
github-actions[bot]
dd5d0669b1 Update translations [skip ci] (#6661)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-16 10:29:40 +02:00
Clément Pasteau
e681632e60 Fix correctly parsing custom messages (#6662)
Do not show in changelog
2024-06-14 18:45:16 +02:00
AlexandreS
0bcb219e00 Inform lobbies iframe if opened from preview in native mobile app (#6659)
Don't show in changelog
2024-06-14 17:09:55 +02:00
D8H
6d5e9c1676 Fix regression on activate behavior action parameter (typo) (#6660)
- Don't show in changelog
2024-06-14 17:06:26 +02:00
github-actions[bot]
5ded5648bf Update translations [skip ci] (#6658)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-06-14 16:39:48 +02:00
AlexandreS
687f926bc5 Add logic to copy lobby configuration when opening an example (#6657)
Don't show in changelog
2024-06-14 15:41:58 +02:00
github-actions[bot]
4a2d573956 Update translations [skip ci] (#6641)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-06-14 15:39:12 +02:00
Clément Pasteau
ce77414f85 Allow assigning variable owners for multiplayer games (#6651)
* Actions can be used at the beginning of the game or a scene, to define scene and global variables as synchronised by another player, or not synchronised at all
2024-06-14 15:37:37 +02:00
D8H
298ff6311d Fix game properties editor to avoid empty values to make project saving fail (#6656) 2024-06-14 13:04:47 +02:00
Clément Pasteau
ca0a000ae1 Use a unique key for root extension configuration row (#6655)
Do not show in changelog
2024-06-14 11:21:58 +02:00
227 changed files with 13140 additions and 8436 deletions

View File

@@ -11,13 +11,24 @@
version: 2.1
orbs:
aws-cli: circleci/aws-cli@2.0.6
macos: circleci/macos@2.5.1 # For Rosetta (see below)
node: circleci/node@5.2.0 # For a recent npm version (see below)
jobs:
# Build the **entire** app for macOS.
build-macos:
macos:
xcode: 14.2.0
resource_class: macos.m1.large.gen1
steps:
- checkout
# Install Rosetta for AWS CLI and disable TSO to speed up S3 uploads (https://support.circleci.com/hc/en-us/articles/19334402064027-Troubleshooting-slow-uploads-to-S3-for-jobs-using-an-m1-macOS-resource-class)
- macos/install-rosetta
- run: sudo sysctl net.inet.tcp.tso=0
# Install a recent version of npm to workaround a notarization issue because of a symlink made by npm: https://github.com/electron-userland/electron-builder/issues/7755
# Node.js v20.14.0 comes with npm v10.7.0.
- node/install:
node-version: "20.14.0"
# System dependencies (for Emscripten and upload)
- run:

View File

@@ -17,11 +17,11 @@ cache:
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- ubuntu-toolchain-r-test
packages:
# Build dependencies:
- cmake
- p7zip-full
# Build dependencies:
- cmake
- p7zip-full
before_install:
# This workaround is required to avoid libstdc++ errors (Emscripten requires a recent version of libstdc++)
@@ -29,47 +29,48 @@ before_install:
- sudo dpkg --force-all -i libstdc++6
install:
# Ensure we use a recent version of Node.js (and npm).
# Ensure we use a recent version of Node.js (and npm).
- nvm install v16 && nvm use v16
#Compile the tests only for GDCore
#Compile the tests only for GDCore
- mkdir .build-tests
- cd .build-tests
- cmake -DBUILD_GDJS=FALSE -DBUILD_TESTS=TRUE -DCMAKE_CXX_COMPILER=$(which $CXX) -DCMAKE_C_COMPILER=$(which $CC) ..
- make -j 4
- cd ..
# Install Emscripten (for GDevelop.js)
- git clone https://github.com/juj/emsdk.git
# Install Emscripten (for GDevelop.js)
# Specify the tag for the core repository to avois breaking changes.
- git clone --depth 1 --branch 3.1.21 https://github.com/juj/emsdk.git
- cd emsdk && ./emsdk install 3.1.21 && ./emsdk activate 3.1.21 && cd ..
# Install GDevelop.js dependencies
# Install GDevelop.js dependencies
- cd GDevelop.js && npm install && cd ..
# Build GDevelop.js
# (in a subshell to avoid Emscripten polluting the Node.js and npm version for the rest of the build)
# Build GDevelop.js
# (in a subshell to avoid Emscripten polluting the Node.js and npm version for the rest of the build)
- (set -e; cd GDevelop.js && source ../emsdk/emsdk_env.sh && npm run build && cd ..)
# Install newIDE tests dependencies
# Install newIDE tests dependencies
- npm -v
- cd newIDE/app && npm install
- cd ../..
# Install GDJS tests dependencies
# Install GDJS tests dependencies
- cd GDJS && npm install && cd tests && npm install
- cd ../..
script:
# GDCore tests:
# GDCore tests:
- cd .build-tests
- Core/GDCore_tests
- cd ..
# GDevelop.js tests
# GDevelop.js tests
- cd GDevelop.js
- npm test
- cd ..
# newIDE tests:
# newIDE tests:
- cd newIDE/app
- npm test
- npm run flow
- npm run check-format
- npm run check-script-types
- cd ../..
# GDJS tests:
# GDJS tests:
- cd GDJS
- npm run check-format
- cd ..

View File

@@ -709,11 +709,14 @@ EventsCodeGenerator::GenerateCallback(
const gd::String actionsDeclarationsCode =
GenerateObjectsDeclarationCode(callbackContext);
const gd::String callbackCode =
callbackFunctionName + " = function (" +
GenerateEventsParameters(callbackContext) + ") {\n" +
restoreLocalVariablesCode +
actionsDeclarationsCode + actionsCode + "}\n";
const gd::String clearLocalVariablesCode =
GenerateLocalVariablesStackAccessor() + ".length = 0;\n";
const gd::String callbackCode = callbackFunctionName + " = function (" +
GenerateEventsParameters(callbackContext) +
") {\n" + restoreLocalVariablesCode +
actionsDeclarationsCode + actionsCode +
clearLocalVariablesCode + "}\n";
AddCustomCodeOutsideMain(callbackCode);

View File

@@ -58,6 +58,7 @@ struct GD_CORE_API ExpressionParserError {
MalformedObjectParameter,
UnknownParameterType,
MissingBehavior,
VariableNameCollision,
};
ExpressionParserError(gd::ExpressionParserError::ErrorType type_,

View File

@@ -1377,7 +1377,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Variables"),
"res/actions/var.png")
.AddParameter("object", _("Object"))
.AddParameter("objectvar", _("Variable"));
.AddParameter("objectvar", _("Variable"))
.SetRelevantForFunctionEventsOnly();
obj.AddExpression("ObjectTimerElapsedTime",
_("Object timer value"),

View File

@@ -231,8 +231,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"it is a text (string)."),
_("Arrays and structures"),
"res/actions/var.png")
.AddParameter("variable", _("Array variable"))
.SetRelevantForLayoutEventsOnly();
.AddParameter("variable", _("Array variable"));
extension
.AddExpression(
@@ -242,8 +241,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"it is a number."),
_("Arrays and structures"),
"res/actions/var.png")
.AddParameter("variable", _("Array variable"))
.SetRelevantForLayoutEventsOnly();
.AddParameter("variable", _("Array variable"));
extension
.AddStrExpression(
@@ -253,8 +251,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"it is a text (string)."),
_("Arrays and structures"),
"res/actions/var.png")
.AddParameter("variable", _("Array variable"))
.SetRelevantForLayoutEventsOnly();
.AddParameter("variable", _("Array variable"));
extension
.AddExpression(
@@ -264,8 +261,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"it is a number."),
_("Arrays and structures"),
"res/actions/var.png")
.AddParameter("variable", _("Array variable"))
.SetRelevantForLayoutEventsOnly();
.AddParameter("variable", _("Array variable"));
// Legacy instructions
@@ -839,7 +835,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Number of children"),
_("Number of children in a scene array or "
"structure variable"),
_("External variables/Scene variables/Arrays and structures"),
_("Arrays and structures"),
"res/actions/var.png")
.AddParameter("variable", _("Array or structure variable"), "AllowUndeclaredVariable");

View File

@@ -203,13 +203,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
if (parentType == Type::Variable) {
childType = parentType;
if (!currentParameterExtraInfo || *currentParameterExtraInfo != "AllowUndeclaredVariable") {
const auto& variablesContainersList = projectScopedContainers.GetVariablesContainersList();
if (!variablesContainersList.Has(node.name)) {
RaiseUndeclaredVariableError(_("No variable with this name found."), node.location, node.name);
}
}
CheckVariableExistence(node.location, node.name);
if (node.child) {
node.child->Visit(*this);
}
@@ -333,12 +327,7 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
}
}
else if (parentType == Type::Variable) {
if (!currentParameterExtraInfo || *currentParameterExtraInfo != "AllowUndeclaredVariable") {
const auto& variablesContainersList = projectScopedContainers.GetVariablesContainersList();
if (!variablesContainersList.Has(node.identifierName)) {
RaiseUndeclaredVariableError(_("No variable with this name found."), node.location, node.identifierName);
}
}
CheckVariableExistence(node.location, node.identifierName);
}
else if (parentType != Type::Object && parentType != Type::LegacyVariable) {
// It can't happen.
@@ -381,6 +370,45 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
Type ValidateFunction(const gd::FunctionCallNode& function);
bool ValidateObjectVariableOrVariableOrProperty(const gd::IdentifierNode& identifier);
void CheckVariableExistence(const ExpressionParserLocation &location, const gd::String& name) {
if (!currentParameterExtraInfo || *currentParameterExtraInfo != "AllowUndeclaredVariable") {
projectScopedContainers.MatchIdentifierWithName<void>(
name,
[&]() {
// This represents an object.
RaiseVariableNameCollisionError(
_("This variable has the same name as an object. Consider "
"renaming one or the other."),
location, name);
},
[&]() {
// This is a variable.
},
[&]() {
// This is a property.
// This error won't happen unless the priority is changed.
RaiseVariableNameCollisionError(
_("This variable has the same name as a property. Consider "
"renaming one or the other."),
location, name);
},
[&]() {
// This is a parameter.
// This error won't happen unless the priority is changed.
RaiseVariableNameCollisionError(
_("This variable has the same name as a parameter. Consider "
"renaming one or the other."),
location, name);
},
[&]() {
// This is something else.
RaiseUndeclaredVariableError(
_("No variable with this name found."), location,
name);
});
}
}
void ReportAnyError(const ExpressionNode& node, bool isFatal = true) {
if (node.diagnostic) {
// Syntax errors are holden by the AST nodes.
@@ -424,6 +452,14 @@ class GD_CORE_API ExpressionValidator : public ExpressionParser2NodeWorker {
message, location, true, variableName, objectName);
}
void RaiseVariableNameCollisionError(const gd::String &message,
const ExpressionParserLocation &location,
const gd::String &variableName,
const gd::String &objectName = "") {
RaiseError(gd::ExpressionParserError::ErrorType::VariableNameCollision,
message, location, false, variableName, objectName);
}
void RaiseTypeError(const gd::String &message,
const ExpressionParserLocation &location,
bool isFatal = true) {

View File

@@ -20,7 +20,7 @@ namespace gd {
void EventsFunctionTools::FreeEventsFunctionToObjectsContainer(
const gd::Project& project,
const gd::EventsFunctionsContainer functionContainer,
const gd::EventsFunctionsContainer& functionContainer,
const gd::EventsFunction& eventsFunction,
gd::ObjectsContainer& outputObjectsContainer) {
// Functions scope for objects is defined according

View File

@@ -35,7 +35,7 @@ class GD_CORE_API EventsFunctionTools {
*/
static void FreeEventsFunctionToObjectsContainer(
const gd::Project& project,
const gd::EventsFunctionsContainer functionContainer,
const gd::EventsFunctionsContainer& functionContainer,
const gd::EventsFunction& eventsFunction,
gd::ObjectsContainer& outputObjectsContainer);

View File

@@ -80,7 +80,7 @@ TEST_CASE("EventsList", "[common][events]") {
#if defined(WINDOWS)
REQUIRE(3000 >= endMemory - startMemory);
#else
REQUIRE(1600 >= endMemory - startMemory);
REQUIRE(1650 >= endMemory - startMemory);
#endif
}
}

View File

@@ -2785,6 +2785,33 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
"No variable with this name found.");
}
SECTION("Variable name collision with an object") {
auto node = parser.ParseExpression("MySpriteObject");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers,
"variable");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
REQUIRE(validator.GetAllErrors().size() == 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"This variable has the same name as an object. Consider renaming "
"one or the other.");
}
SECTION("Variable name collision with an object (with child-variables)") {
auto node = parser.ParseExpression("MySpriteObject.MyChild.MyChild");
REQUIRE(node != nullptr);
gd::ExpressionValidator validator(platform, projectScopedContainers, "variable");
node->Visit(validator);
REQUIRE(validator.GetFatalErrors().size() == 0);
REQUIRE(validator.GetAllErrors().size() == 1);
REQUIRE(validator.GetAllErrors()[0]->GetMessage() ==
"This variable has the same name as an object. Consider renaming "
"one or the other.");
}
SECTION("Declared scene variable") {
auto node = parser.ParseExpression("MySceneVariable");
REQUIRE(node != nullptr);

View File

@@ -126,9 +126,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): Object3DNetworkSyncData {
getNetworkSyncData(): Object3DNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
z: this.getZ(),
w: this.getWidth(),
h: this.getHeight(),
@@ -141,8 +141,8 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
super.updateFromObjectNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.w !== undefined) this.setWidth(networkSyncData.w);
if (networkSyncData.h !== undefined) this.setHeight(networkSyncData.h);

View File

@@ -414,9 +414,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): Cube3DObjectNetworkSyncData {
getNetworkSyncData(): Cube3DObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
mt: this._materialType,
fo: this._facesOrientation,
bfu: this._backFaceUpThroughWhichAxisRotation,
@@ -426,10 +426,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: Cube3DObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;

View File

@@ -175,9 +175,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): Model3DObjectNetworkSyncData {
getNetworkSyncData(): Model3DObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
mt: this._materialType,
op: this._originPoint,
cp: this._centerPoint,
@@ -188,10 +188,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: Model3DObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;

View File

@@ -131,9 +131,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): BBTextObjectNetworkSyncData {
getNetworkSyncData(): BBTextObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
text: this._text,
o: this._opacity,
c: this._color,
@@ -146,10 +146,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: BBTextObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (this._text !== undefined) {
this.setBBText(networkSyncData.text);
}

View File

@@ -147,9 +147,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): BitmapTextObjectNetworkSyncData {
getNetworkSyncData(): BitmapTextObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
text: this._text,
opa: this._opacity,
tint: this._tint,
@@ -162,10 +162,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: BitmapTextObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (this._text !== undefined) {
this.setText(networkSyncData.text);
}

View File

@@ -88,18 +88,16 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): LightNetworkSyncData {
getNetworkSyncData(): LightNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
rad: this.getRadius(),
col: this.getColor(),
};
}
updateFromObjectNetworkSyncData(
networkSyncData: LightNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(networkSyncData: LightNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.rad !== undefined) {
this.setRadius(networkSyncData.rad);

View File

@@ -39,15 +39,15 @@ module.exports = {
'Open the game lobbies window, where players can join lobbies or see the one they are in.'
),
_('Open the game lobbies'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -56,6 +56,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.openLobbiesWindow');
@@ -65,7 +66,7 @@ module.exports = {
_('Allow players to close the lobbies window'),
_('Allow players to close the lobbies window. Allowed by default.'),
_('Allow players to close the lobbies window: _PARAM1_'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -73,8 +74,8 @@ module.exports = {
.addParameter('yesorno', _('Show close button'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -83,6 +84,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.showLobbiesCloseButton');
@@ -94,14 +96,14 @@ module.exports = {
'End the lobby game. This will trigger the "Lobby game has just ended" condition.'
),
_('End the lobby game'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -110,9 +112,38 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.endLobbyGame');
extension
.addAction(
'LeaveGameLobby',
_('Leave Game Lobby'),
_(
'Leave the current game lobby. This will trigger the "Player has left" condition on the other players, and the "Lobby game has ended" condition on the player leaving.'
),
_('Leave the game lobby'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.leaveGameLobby');
extension
.addAction(
'SendMessage',
@@ -121,7 +152,7 @@ module.exports = {
"Send a custom message to other players in the lobby, with an automatic retry system if it hasn't been received. Use with the condition 'Message has been received' to know when the message has been properly processed by the host."
),
_('Send message _PARAM0_ to other players with content _PARAM1_'),
'',
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -129,8 +160,8 @@ module.exports = {
.addParameter('string', _('Message name'), '', false)
.addParameter('string', _('Message content'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -139,8 +170,73 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.sendMessage');
.setFunctionName('gdjs.multiplayerMessageManager.sendCustomMessage');
extension
.addAction(
'SendVariableMessage',
_('Send custom message to other players with a variable'),
_(
"Send a custom message to other players in the lobby containing a variable, with an automatic retry system if it hasn't been received. Use with the condition 'Message has been received' to know when the message has been properly processed by the host."
),
_('Send message _PARAM0_ to other players with variable _PARAM1_'),
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.setHelpPath('/all-features/multiplayer')
.addParameter('string', _('Message name'), '', false)
.addParameter('variable', _('Variable'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerMessageManager.sendVariableCustomMessage'
);
extension
.addAction(
'GetMessageVariable',
_('Get message variable'),
_(
"Store the data of the specified message in a variable. Use with the condition 'Message has been received' to know when the message has been properly processed by the host."
),
_('Save message _PARAM0_ data in _PARAM1_'),
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.setHelpPath('/all-features/multiplayer')
.addParameter('string', _('Message name'), '', false)
.addParameter('variable', _('Variable'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerMessageManager.getVariableCustomMessageData'
);
extension
.addCondition(
@@ -148,14 +244,14 @@ module.exports = {
_('Lobbies window is open'),
_('Check if the lobbies window is open.'),
_('Lobbies window is open'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -164,6 +260,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.isLobbiesWindowOpen');
@@ -173,13 +270,13 @@ module.exports = {
_('Lobby game has just started'),
_('Check if the lobby game has just started.'),
_('Lobby game has started'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -188,6 +285,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.hasLobbyGameJustStarted');
@@ -197,13 +295,13 @@ module.exports = {
_('Lobby game is running'),
_('Check if the lobby game is running.'),
_('Lobby game is running'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -212,6 +310,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.isLobbyGameRunning');
@@ -221,13 +320,13 @@ module.exports = {
_('Lobby game has just ended'),
_('Check if the lobby game has just ended.'),
_('Lobby game has ended'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -236,6 +335,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.hasLobbyGameJustEnded');
@@ -247,14 +347,14 @@ module.exports = {
'Check if a custom message has been received from another player. Will be true only for one frame.'
),
_('Message _PARAM0_ has been received'),
'',
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -263,8 +363,11 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasMessageBeenReceived');
.setFunctionName(
'gdjs.multiplayerMessageManager.hasCustomMessageBeenReceived'
);
extension
.addCondition(
@@ -272,13 +375,13 @@ module.exports = {
_('Player is host'),
_('Check if the player is the host. (Player 1 is the host)'),
_('Player is host'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -287,6 +390,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.isPlayerHost');
@@ -296,13 +400,13 @@ module.exports = {
_('Any player has left'),
_('Check if any player has left the lobby.'),
_('Any player has left'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -311,6 +415,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasAnyPlayerLeft');
@@ -320,14 +425,14 @@ module.exports = {
_('Player has left'),
_('Check if the player has left the lobby.'),
_('Player _PARAM0_ has left'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.addParameter('number', _('Player number'), '', false)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -336,6 +441,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.hasPlayerLeft');
@@ -346,13 +452,13 @@ module.exports = {
_(
'Returns the data received when the specified message was received from another player.'
),
'',
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -361,21 +467,46 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.getMessageData');
.setFunctionName('gdjs.multiplayerMessageManager.getCustomMessageData');
extension
.addExpression(
'MessageSender',
_('Message sender'),
_('Returns the player number of the sender of the specified message.'),
_('Advanced'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.getCustomMessageSender');
extension
.addExpressionAndCondition(
'number',
'NumberOfPlayersInLobby',
'PlayersInLobbyCount',
_('Number of players in lobby'),
_('the number of players in the lobby'),
_('the number of players in the lobby'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -384,22 +515,23 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('gdjs.multiplayer.getNumberOfPlayersInLobby');
.setFunctionName('gdjs.multiplayer.getPlayersInLobbyCount');
extension
.addExpressionAndCondition(
'number',
'PlayerNumber',
_('Player number in lobby'),
_('the player number in the lobby (1, 2, ...)'),
_('the player number in the lobby'),
'',
'CurrentPlayerNumber',
_('Current player number in lobby'),
_('the current player number in the lobby (1, 2, ...)'),
_('the current player number in the lobby'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -408,16 +540,17 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setFunctionName('gdjs.multiplayer.getPlayerNumber');
.setFunctionName('gdjs.multiplayer.getCurrentPlayerNumber');
extension
.addStrExpression(
'PlayerUsername',
_('Player username in lobby'),
_('Get the username of the player in the lobby.'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter(
@@ -427,8 +560,8 @@ module.exports = {
false
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -437,15 +570,39 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.getPlayerUsername');
extension
.addStrExpression(
'CurrentPlayerUsername',
_('Current player username in lobby'),
_('Get the username of the current player in the lobby.'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.getCurrentPlayerUsername');
extension
.addExpression(
'PlayerPing',
_('Player ping in lobby'),
_('Get the ping of the player in the lobby.'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter(
@@ -455,8 +612,8 @@ module.exports = {
false
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -465,9 +622,158 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.getPlayerPing');
extension
.addExpression(
'CurrentPlayerPing',
_('Current player ping in lobby'),
_('Get the ping of the current player in the lobby.'),
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayerMessageManager.getCurrentPlayerPing');
extension
.addExpressionAndConditionAndAction(
'number',
'PlayerVariableOwnership',
_('Player variable ownership'),
_('the player owning the variable'),
_('the player owning the variable _PARAM1_'),
_('Variables'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable'), '', false)
.useStandardParameters(
'number',
gd.ParameterOptions.makeNewOptions().setDescription(_('Player number'))
)
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerVariablesManager.setPlayerVariableOwnership'
)
.setGetter('gdjs.multiplayerVariablesManager.getPlayerVariableOwnership');
extension
.addAction(
'TakeVariableOwnership',
_('Take ownership of variable'),
_(
'Take the ownership of the variable. It will then be synchronized to other players, with the current player as the owner.'
),
_('Take ownership of _PARAM1_'),
_('Variables'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerVariablesManager.takeVariableOwnership'
);
extension
.addAction(
'RemoveVariableOwnership',
_('Remove ownership of variable'),
_(
'Remove the ownership of the variable. It will still be synchronized to other players, but the host owns it.'
),
_('Remove ownership of _PARAM1_'),
_('Variables'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerVariablesManager.removeVariableOwnership'
);
extension
.addAction(
'DisableVariableSynchronization',
_('Disable variable synchronization'),
_(
'Disable synchronization of the variable over the network. It will not be sent to other players anymore.'
),
_('Disable synchronization of _PARAM1_'),
_('Variables'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName(
'gdjs.multiplayerVariablesManager.disableVariableSynchronization'
);
// Multiplayer object behavior
const multiplayerObjectBehavior = new gd.BehaviorJsImplementation();
@@ -559,8 +865,8 @@ module.exports = {
multiplayerObjectBehavior,
sharedData
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -568,6 +874,8 @@ module.exports = {
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.addIncludeFile(
'Extensions/Multiplayer/multiplayerobjectruntimebehavior.js'
);
@@ -578,8 +886,8 @@ module.exports = {
'PlayerObjectOwnership',
_('Player object ownership'),
_('the player owning the object'),
_('the player owning the object'),
'',
_('the player owning the instance'),
_('Multiplayer'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('object', _('Object'), '', false)
@@ -600,7 +908,7 @@ module.exports = {
'Check if the object is owned by the current player, as a player or the host.'
),
_('Object _PARAM0_ is owned by current player'),
'',
_('Multiplayer'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -622,7 +930,7 @@ module.exports = {
'Take the ownership of the object. It will then be synchronized to other players, with the current player as the owner.'
),
_('Take ownership of _PARAM0_'),
'',
_('Multiplayer'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -644,7 +952,7 @@ module.exports = {
'Remove the ownership of the object from the player. It will still be synchronized to other players, but the host owns it.'
),
_('Remove ownership of _PARAM0_'),
'',
_('Multiplayer'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -658,6 +966,30 @@ module.exports = {
.markAsAdvanced()
.setFunctionName('removeObjectOwnership');
behavior
.addScopedAction(
'EnableBehaviorSynchronization',
_('Enable (or disable) the synchronization of a behavior'),
_(
"Enable or disable the synchronization of a behavior over the network. If disabled, the behavior's current state will not be sent to other players anymore."
),
_('Enable synchronization of _PARAM2_ for _PARAM0_: _PARAM3_'),
_('Multiplayer'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('object', _('Object'), '', false)
.addParameter(
'behavior',
_('Multiplayer behavior'),
'MultiplayerObjectBehavior',
false
)
.addParameter('behavior', _('Object behavior'), '', false)
.addParameter('yesorno', _('Enable synchronization'), '', false)
.markAsAdvanced()
.setFunctionName('enableBehaviorSynchronization');
return extension;
},
runExtensionSanityTests: function (gd, extension) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,273 @@
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
export type MultiplayerVariablesManager = ReturnType<
typeof makeMultiplayerVariablesManager
>;
export const makeMultiplayerVariablesManager = () => {
const variableOwnershipChangesToSyncAtEndOfFrame: {
[variableNetworkId: string]: {
variableName: string;
sceneNetworkId?: string; // If not defined, the variable is global.
previousVariableOwner: number;
newVariableOwner: number;
};
} = {};
const addVariableOwnershipChangeToSync = function ({
variableNetworkId,
previousVariableOwner,
newVariableOwner,
}: {
variableNetworkId: string;
previousVariableOwner: number;
newVariableOwner: number;
}) {
// If the variable is already planned to be synchronized, update it with the new owner.
if (variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId]) {
variableOwnershipChangesToSyncAtEndOfFrame[
variableNetworkId
].newVariableOwner = newVariableOwner;
return;
}
variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId] = {
variableName: variableNetworkId,
previousVariableOwner: previousVariableOwner,
newVariableOwner: newVariableOwner,
};
};
const getVariableTypeAndNameFromNetworkId = function (
variableNetworkId: string
): { type: string; name: string; containerId: string } {
const parts = variableNetworkId.split('_');
if (parts.length < 2) {
throw new Error(
'Trying to get the variable type from a network id that is not a valid variable network id.'
);
}
return {
type: parts[0] === 'game' ? 'global' : 'scene',
name: parts.slice(1).join('_'),
containerId: parts[0],
};
};
// A variable network id is a combination of the scene network id if it's a scene variable,
// and the variable name, or "game" and the variable name if it's a global variable.
const _guessVariableNetworkIdFromSceneAndGame = function (
variable: gdjs.Variable,
currentScene: gdjs.RuntimeScene
): string | undefined {
const currentSceneVariables = currentScene.getVariables();
if (currentSceneVariables.hasVariable(variable)) {
// Scene variable.
const sceneNetworkId = currentScene.networkId;
if (!sceneNetworkId) {
// Variable is being synchronized but the scene has no networkId yet.
// It should have one assigned as soon as the scene is synchronized.
// Skipping.
debugLogger.info(
'Variable is being synchronized but the scene has no networkId yet.'
);
return;
}
const variableName = currentSceneVariables.getVariableNameInContainerByLoopingThroughAllVariables(
variable
);
if (!variableName) {
logger.error('Variable is being synchronized but has no name.');
return;
}
return sceneNetworkId + '_' + variableName;
}
const runtimeGame = currentScene.getGame();
const runtimeGameVariables = runtimeGame.getVariables();
if (runtimeGameVariables.hasVariable(variable)) {
// Global variable.
// TODO: prevent returning a networkID if this is not a root variable.
const variableName = runtimeGameVariables.getVariableNameInContainerByLoopingThroughAllVariables(
variable
);
if (!variableName) {
logger.error('Variable is being synchronized but has no name.');
return;
}
return 'game_' + variableName;
}
logger.error(
'Trying to modify synchronization of a variable that is not a scene or global variable.'
);
return;
};
const getPlayerVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
return variable.getPlayerOwnership();
};
const setPlayerVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable,
newVariablePlayerNumber: number
) {
debugLogger.info(
`Setting ownership of variable to player ${newVariablePlayerNumber}.`
);
if (newVariablePlayerNumber < 0) {
logger.error(
'Invalid player number (' +
newVariablePlayerNumber +
') when setting ownership of a variable.'
);
return;
}
const previousVariablePlayerNumber = variable.getPlayerOwnership();
if (previousVariablePlayerNumber === null) {
logger.error(
'Cannot change ownership of a variable that is not synchronized.'
);
return;
}
variable.setPlayerOwnership(newVariablePlayerNumber);
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
// If the lobby game is not running, do not try to update the ownership over the network,
// as the game may update variable ownerships before the lobby game starts.
if (!gdjs.multiplayer.isLobbyGameRunning()) {
return;
}
if (newVariablePlayerNumber !== currentPlayerNumber) {
// If we are not the new owner, we should not send a message to the host to change the ownership.
// Just return and wait to receive an update message to reconcile the variable.
return;
}
const variableNetworkId = _guessVariableNetworkIdFromSceneAndGame(
variable,
runtimeScene
);
const sceneNetworkId = runtimeScene.networkId;
if (!variableNetworkId || !sceneNetworkId) {
// An error was already logged.
return;
}
const { type: variableType } = getVariableTypeAndNameFromNetworkId(
variableNetworkId
);
debugLogger.info(
`Adding variable to be synchronized: ${variableNetworkId} (type: ${variableType}) from owner ${previousVariablePlayerNumber} to ${newVariablePlayerNumber}.`
);
addVariableOwnershipChangeToSync({
variableNetworkId,
previousVariableOwner: previousVariablePlayerNumber,
newVariableOwner: newVariablePlayerNumber,
});
};
const takeVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
setPlayerVariableOwnership(runtimeScene, variable, currentPlayerNumber);
};
const removeVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
setPlayerVariableOwnership(runtimeScene, variable, 0);
};
const disableVariableSynchronization = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
variable.disableSynchronization();
};
const handleChangeVariableOwnerMessagesToSend = function () {
if (!gdjs.multiplayer.isLobbyGameRunning()) {
return;
}
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
for (const variableNetworkId in variableOwnershipChangesToSyncAtEndOfFrame) {
const variableData =
variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId];
const {
messageName,
messageData,
} = gdjs.multiplayerMessageManager.createChangeVariableOwnerMessage({
variableNetworkId,
variableOwner: variableData.previousVariableOwner,
newVariableOwner: variableData.newVariableOwner,
});
// Before sending the change owner message, if we are becoming the new owner,
// we want to ensure this message is acknowledged, by everyone we're connected to.
if (variableData.newVariableOwner === currentPlayerNumber) {
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const variableOwnerChangedMessageName = gdjs.multiplayerMessageManager.createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage(
messageName
);
gdjs.multiplayerMessageManager.addExpectedMessageAcknowledgement({
originalMessageName: messageName,
originalData: messageData,
expectedMessageName: variableOwnerChangedMessageName,
otherPeerIds,
// If we are not the host and don't receive an acknowledgement from the host, we should cancel the ownership change.
shouldCancelMessageIfTimesOut: currentPlayerNumber !== 1,
});
}
debugLogger.info('Sending change owner message', messageName);
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
gdjs.multiplayerMessageManager.sendDataTo(
connectedPeerIds,
messageName,
messageData
);
// Remove the variable from the list of variables ownership changes to sync.
delete variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId];
}
};
return {
getVariableTypeAndNameFromNetworkId,
getPlayerVariableOwnership,
setPlayerVariableOwnership,
takeVariableOwnership,
removeVariableOwnership,
disableVariableSynchronization,
handleChangeVariableOwnerMessagesToSend,
};
};
/**
* The MultiplayerVariablesManager used by the game.
*/
export let multiplayerVariablesManager = makeMultiplayerVariablesManager();
}

View File

@@ -5,6 +5,7 @@
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
const getTimeNow =
window.performance && typeof window.performance.now === 'function'
? window.performance.now.bind(window.performance)
@@ -92,7 +93,7 @@ namespace gdjs {
// if it has not been assigned a networkId after a short delay.
this._destroyInstanceTimeoutId = setTimeout(() => {
if (!owner.networkId && gdjs.multiplayer.isLobbyGameRunning()) {
logger.info(
debugLogger.info(
`Lobby game is running and object ${owner.getName()} has not been assigned a networkId after a short delay, destroying it.`
);
owner.deleteFromScene(instanceContainer);
@@ -100,20 +101,22 @@ namespace gdjs {
}, this._timeBeforeDestroyingObjectWithoutNetworkIdInMs);
}
private _sendDataToPeersWithIncreasedClock(
private _sendDataToPeersWithIncreasedClock = async (
messageName: string,
data: Object
) {
) => {
this._clock++;
data['_clock'] = this._clock;
const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers();
for (const peerId of connectedPeerIds) {
gdjs.multiplayerMessageManager.sendDataTo(peerId, messageName, data);
}
}
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
await gdjs.multiplayerMessageManager.sendDataTo(
connectedPeerIds,
messageName,
data
);
};
private _isOwnerAsPlayerOrHost() {
const currentPlayerNumber = gdjs.multiplayer.getPlayerNumber();
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const isOwnerOfObject =
currentPlayerNumber === this.playerNumber || // Player as owner.
@@ -151,10 +154,7 @@ namespace gdjs {
}
// private _logToConsoleWithThrottle(message: string) {
// if (
// getTimeNow() - this._lastLogTimestamp >
// 1000 / this._logTickRate
// ) {
// if (getTimeNow() - this._lastLogTimestamp > 1000 / this._logTickRate) {
// logger.info(message);
// this._lastLogTimestamp = getTimeNow();
// }
@@ -255,7 +255,7 @@ namespace gdjs {
this.playerNumber !== 0 && // Host is always connected.
!gdjs.multiplayerMessageManager.isPlayerConnected(this.playerNumber)
) {
logger.info(
debugLogger.info(
`Player number ${this.playerNumber} does not exist in the lobby at the moment. Destroying the object.`
);
this.owner.deleteFromScene(this.owner.getInstanceContainer());
@@ -272,16 +272,18 @@ namespace gdjs {
return;
}
const instanceNetworkId = this._getOrCreateInstanceNetworkId();
const objectName = this.owner.getName();
const objectNetworkSyncData = this.owner.getNetworkSyncData();
// this._logToConsoleWithThrottle(
// `Synchronizing object ${this.owner.getName()} (instance ${
// this.owner.networkId
// }) with player ${this.playerNumber}`
// }) with player ${this.playerNumber} and data ${JSON.stringify(
// objectNetworkSyncData
// )}`
// );
const instanceNetworkId = this._getOrCreateInstanceNetworkId();
const objectName = this.owner.getName();
const objectNetworkSyncData = this.owner.getObjectNetworkSyncData();
const areBasicObjectNetworkSyncDataDifferent = this._isBasicObjectNetworkSyncDataDifferentFromLastSync(
{
x: objectNetworkSyncData.x,
@@ -290,6 +292,7 @@ namespace gdjs {
zo: objectNetworkSyncData.zo,
a: objectNetworkSyncData.a,
hid: objectNetworkSyncData.hid,
lay: objectNetworkSyncData.lay,
if: objectNetworkSyncData.if,
pfx: objectNetworkSyncData.pfx,
pfy: objectNetworkSyncData.pfy,
@@ -345,7 +348,7 @@ namespace gdjs {
const {
messageName: updateMessageName,
messageData: updateMessageData,
} = gdjs.multiplayerMessageManager.createUpdateObjectMessage({
} = gdjs.multiplayerMessageManager.createUpdateInstanceMessage({
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
@@ -368,6 +371,7 @@ namespace gdjs {
zo: objectNetworkSyncData.zo,
a: objectNetworkSyncData.a,
hid: objectNetworkSyncData.hid,
lay: objectNetworkSyncData.lay,
if: objectNetworkSyncData.if,
pfx: objectNetworkSyncData.pfx,
pfy: objectNetworkSyncData.pfy,
@@ -407,7 +411,7 @@ namespace gdjs {
return;
}
// For desruction of objects, we allow the host to destroy the object even if it is not the owner.
// For destruction of objects, we allow the host to destroy the object even if it is not the owner.
// This is particularly helpful when a player disconnects, so the host can destroy the object they were owning.
if (!this._isOwnerAsPlayerOrHost() && !gdjs.multiplayer.isPlayerHost()) {
return;
@@ -418,7 +422,7 @@ namespace gdjs {
// If it had no networkId, then it was not synchronized and we don't need to send a message.
if (!instanceNetworkId) {
logger.info(
debugLogger.info(
`Destroying object ${objectName} without networkId, no need to send a message.`
);
return;
@@ -434,11 +438,11 @@ namespace gdjs {
const {
messageName: updateMessageName,
messageData: updateMessageData,
} = gdjs.multiplayerMessageManager.createUpdateObjectMessage({
} = gdjs.multiplayerMessageManager.createUpdateInstanceMessage({
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
objectNetworkSyncData: this.owner.getObjectNetworkSyncData(),
objectNetworkSyncData: this.owner.getNetworkSyncData(),
sceneNetworkId,
});
this._sendDataToPeersWithIncreasedClock(
@@ -451,17 +455,17 @@ namespace gdjs {
// If we are player 1, we are connected to everyone, so we expect an acknowledgment from everyone.
// If we are another player, we are only connected to player 1, so we expect an acknowledgment from player 1.
// In both cases, this represents the list of peers the current user is connected to.
const otherPeerIds = gdjs.evtTools.p2p.getAllPeers();
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const {
messageName: destroyMessageName,
messageData: destroyMessageData,
} = gdjs.multiplayerMessageManager.createDestroyObjectMessage({
} = gdjs.multiplayerMessageManager.createDestroyInstanceMessage({
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
sceneNetworkId,
});
const destroyedMessageName = gdjs.multiplayerMessageManager.createObjectDestroyedMessageNameFromDestroyMessage(
const destroyedMessageName = gdjs.multiplayerMessageManager.createInstanceDestroyedMessageNameFromDestroyInstanceMessage(
destroyMessageName
);
gdjs.multiplayerMessageManager.addExpectedMessageAcknowledgement({
@@ -474,6 +478,8 @@ namespace gdjs {
},
expectedMessageName: destroyedMessageName,
otherPeerIds,
// Destruction of objects are not reverted, as they will eventually be recreated by an update message.
shouldCancelMessageIfTimesOut: false,
});
this._sendDataToPeersWithIncreasedClock(
@@ -483,7 +489,7 @@ namespace gdjs {
}
setPlayerObjectOwnership(newObjectPlayerNumber: number) {
logger.info(
debugLogger.info(
`Setting ownership of object ${this.owner.getName()} (networkId: ${
this.owner.networkId
} to player ${newObjectPlayerNumber}.`
@@ -504,7 +510,7 @@ namespace gdjs {
// If the host does not send an acknowledgment, we will revert the ownership.
const previousObjectPlayerNumber = this.playerNumber;
this.playerNumber = newObjectPlayerNumber;
const currentPlayerNumber = gdjs.multiplayer.getPlayerNumber();
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
// If the lobby game is not running, do not try to update the ownership over the network,
// as the game may create & update objects before the lobby game starts.
@@ -514,14 +520,20 @@ namespace gdjs {
let instanceNetworkId = this.owner.networkId;
if (!instanceNetworkId) {
logger.info(
debugLogger.info(
'Object has no networkId, we change the ownership locally, but it will not be synchronized yet if we are not the owner.'
);
if (newObjectPlayerNumber !== gdjs.multiplayer.getPlayerNumber()) {
if (newObjectPlayerNumber !== currentPlayerNumber) {
// If we are not the new owner, we should not send a message to the host to change the ownership.
// Just return and wait to receive an update message to reconcile this object.
return;
}
// If we don't have a networkId, we need to create one now that we are the owner.
// We are probably in a case where we created the object and then changed the ownership.
debugLogger.info(
'We are the new owner, creating a networkId for the object.'
);
instanceNetworkId = this._getOrCreateInstanceNetworkId();
}
const sceneNetworkId = this.owner.getRuntimeScene().networkId;
@@ -532,63 +544,58 @@ namespace gdjs {
const objectName = this.owner.getName();
if (instanceNetworkId) {
// When changing the ownership of an object with a networkId, we send a message to the host to ensure it is aware of the change,
// and can either accept it and broadcast it to other players, or reject it and do nothing with it.
// We expect an acknowledgment from the host, if not, we will retry and eventually revert the ownership.
const {
messageName,
messageData,
} = gdjs.multiplayerMessageManager.createChangeOwnerMessage({
objectOwner: previousObjectPlayerNumber,
objectName,
instanceNetworkId,
newObjectOwner: newObjectPlayerNumber,
instanceX: this.owner.getX(),
instanceY: this.owner.getY(),
sceneNetworkId,
// When changing the ownership of an object with a networkId, we send a message to the host to ensure it is aware of the change,
// and can either accept it and broadcast it to other players, or reject it and do nothing with it.
// We expect an acknowledgment from the host, if not, we will retry and eventually revert the ownership.
const {
messageName,
messageData,
} = gdjs.multiplayerMessageManager.createChangeInstanceOwnerMessage({
objectOwner: previousObjectPlayerNumber,
objectName,
instanceNetworkId,
newObjectOwner: newObjectPlayerNumber,
instanceX: this.owner.getX(),
instanceY: this.owner.getY(),
sceneNetworkId,
});
// Before sending the changeOwner message, if we are becoming the new owner,
// we want to ensure this message is acknowledged, by everyone we're connected to.
// If we are player 1, we are connected to everyone, so we expect an acknowledgment from everyone.
// If we are another player, we are only connected to player 1, so we expect an acknowledgment from player 1.
// In both cases, this represents the list of peers the current user is connected to.
if (newObjectPlayerNumber === currentPlayerNumber) {
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const changeOwnerAcknowledgedMessageName = gdjs.multiplayerMessageManager.createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage(
messageName
);
gdjs.multiplayerMessageManager.addExpectedMessageAcknowledgement({
originalMessageName: messageName,
originalData: {
...messageData,
_clock: this._clock + 1, // Will be incremented by the time the message is sent.
},
expectedMessageName: changeOwnerAcknowledgedMessageName,
otherPeerIds,
// If we are not the host, we should revert the ownership if the host does not acknowledge the change.
shouldCancelMessageIfTimesOut: currentPlayerNumber !== 1,
});
// Before sending the changeOwner message, if we are becoming the new owner,
// we want to ensure this message is acknowledged, by everyone we're connected to.
// If we are player 1, we are connected to everyone, so we expect an acknowledgment from everyone.
// If we are another player, we are only connected to player 1, so we expect an acknowledgment from player 1.
// In both cases, this represents the list of peers the current user is connected to.
if (newObjectPlayerNumber === currentPlayerNumber) {
const otherPeerIds = gdjs.evtTools.p2p.getAllPeers();
const changeOwnerAcknowledgedMessageName = gdjs.multiplayerMessageManager.createObjectOwnerChangedMessageNameFromChangeOwnerMessage(
messageName
);
gdjs.multiplayerMessageManager.addExpectedMessageAcknowledgement({
originalMessageName: messageName,
originalData: {
...messageData,
_clock: this._clock + 1, // Will be incremented by the time the message is sent.
},
expectedMessageName: changeOwnerAcknowledgedMessageName,
otherPeerIds,
// If we are not the host, we should revert the ownership if the host does not acknowledge the change.
shouldCancelMessageIfTimesOut: currentPlayerNumber !== 1,
});
}
logger.info('Sending change owner message', messageName);
this._sendDataToPeersWithIncreasedClock(messageName, messageData);
}
debugLogger.info('Sending change owner message', messageName);
this._sendDataToPeersWithIncreasedClock(messageName, messageData);
// If we are the new owner, also send directly an update of the position,
// so that the object is immediately moved on the screen and we don't wait for the next tick.
if (newObjectPlayerNumber === currentPlayerNumber) {
if (!instanceNetworkId) {
// If we don't have a networkId, we need to create one now that we are the owner.
// We are probably in a case where we created the object and then changed the ownership.
instanceNetworkId = this._getOrCreateInstanceNetworkId();
}
const objectNetworkSyncData = this.owner.getObjectNetworkSyncData();
debugLogger.info(
'Sending update message to move the object immediately.'
);
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const {
messageName: updateMessageName,
messageData: updateMessageData,
} = gdjs.multiplayerMessageManager.createUpdateObjectMessage({
} = gdjs.multiplayerMessageManager.createUpdateInstanceMessage({
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
@@ -616,12 +623,24 @@ namespace gdjs {
}
takeObjectOwnership() {
this.setPlayerObjectOwnership(gdjs.multiplayer.getPlayerNumber());
this.setPlayerObjectOwnership(gdjs.multiplayer.getCurrentPlayerNumber());
}
getActionOnPlayerDisconnect() {
return this.actionOnPlayerDisconnect;
}
enableBehaviorSynchronization(behaviorName: string, enable: boolean) {
const behavior = this.owner.getBehavior(behaviorName);
if (!behavior) {
logger.error(
`Behavior ${behaviorName} does not exist on object ${this.owner.getName()}.`
);
return;
}
behavior.enableSynchronization(enable);
}
}
gdjs.registerBehavior(
'Multiplayer::MultiplayerObjectBehavior',

View File

@@ -1,27 +1,26 @@
namespace gdjs {
declare var cordova: any;
const logger = new gdjs.Logger('Multiplayer');
const multiplayerComponents = gdjs.multiplayerComponents;
type Lobby = {
id: string;
name: string;
status: string;
players: { playerId: string; status: string }[];
};
export namespace multiplayer {
/** Set to true in testing to avoid relying on the multiplayer extension. */
export let disableMultiplayerForTesting = false;
let _isGameRegistered: boolean | null = null;
let _isCheckingIfGameIsRegistered = false;
let _isWaitingForLoginCallback = false;
let _isWaitingForLogin = false;
let _hasLobbyGameJustStarted = false;
export let _isLobbyGameRunning = false;
let _hasLobbyGameJustEnded = false;
let _lobbyId: string | null = null;
let _connectionId: string | null = null;
export let _lobby: {
id: string;
name: string;
status: string;
players: { playerId: string; status: string }[];
} | null = null;
export let _lobby: Lobby | null = null;
let _playerPublicProfiles: { id: string; username?: string }[] = [];
// Communication methods.
@@ -32,6 +31,7 @@ namespace gdjs {
const DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL = 10000;
const DEFAULT_LOBBY_HEARTBEAT_INTERVAL = 30000;
const DEFAULT_COUNTDOWN_SECONDS_TO_START = 5;
// Save if we are on dev environment so we don't need to use the runtimeGame every time.
let isUsingGDevelopDevelopmentEnvironment = false;
@@ -46,16 +46,27 @@ namespace gdjs {
if (disableMultiplayerForTesting) return;
gdjs.multiplayerMessageManager.handleChangeOwnerMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleUpdateObjectMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleCustomMessages();
gdjs.multiplayerMessageManager.handleAcknowledgeMessages();
gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateInstanceMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleCustomMessagesReceived();
gdjs.multiplayerMessageManager.handleAcknowledgeMessagesReceived();
gdjs.multiplayerMessageManager.resendClearOrCancelAcknowledgedMessages(
runtimeScene
);
gdjs.multiplayerMessageManager.handleGameUpdatedMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleSceneUpdatedMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleHeartbeats();
gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(
runtimeScene
);
gdjs.multiplayerMessageManager.handleHeartbeatsToSend();
gdjs.multiplayerMessageManager.handleDisconnectedPeers(runtimeScene);
}
);
@@ -64,11 +75,16 @@ namespace gdjs {
(runtimeScene: gdjs.RuntimeScene) => {
if (disableMultiplayerForTesting) return;
gdjs.multiplayerMessageManager.handleDestroyObjectMessages(
gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(
runtimeScene
);
gdjs.multiplayerVariablesManager.handleChangeVariableOwnerMessagesToSend();
gdjs.multiplayerMessageManager.handleUpdateGameMessagesToSend(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(
runtimeScene
);
gdjs.multiplayerMessageManager.handleUpdateGameMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleUpdateSceneMessages(runtimeScene);
gdjs.multiplayerMessageManager.handleHeartbeatsReceived();
handleLeavingPlayer(runtimeScene);
gdjs.multiplayerMessageManager.clearDisconnectedPeers();
@@ -93,9 +109,9 @@ namespace gdjs {
// Uncomment to test the case of a failing loading:
// return 'https://gd.games.wronglink';
const baseUrl = 'https://gd.games';
// const baseUrl = 'https://gd.games';
// Uncomment to test locally:
// const baseUrl = 'http://localhost:4000';
const baseUrl = 'http://localhost:4000';
const url = new URL(
`${baseUrl}/games/${gameId}/lobbies${_lobbyId ? `/${_lobbyId}` : ''}`
@@ -104,6 +120,9 @@ namespace gdjs {
'gameVersion',
runtimeGame.getGameData().properties.version
);
if (runtimeGame.getAdditionalOptions().nativeMobileApp) {
url.searchParams.set('nativeMobileApp', 'true');
}
url.searchParams.set(
'isPreview',
runtimeGame.isPreview() ? 'true' : 'false'
@@ -146,7 +165,7 @@ namespace gdjs {
/**
* Returns the number of players in the lobby.
*/
export const getNumberOfPlayersInLobby = () => {
export const getPlayersInLobbyCount = () => {
// If the game has not started yet, look at the lobby.
if (!_isLobbyGameRunning && _lobby) {
return _lobby.players.length;
@@ -165,7 +184,7 @@ namespace gdjs {
* Return 0 if the player is not in the lobby.
* Returns 1, 2, 3, ... if the player is in the lobby.
*/
export const getPlayerNumber = () => {
export const getCurrentPlayerNumber = () => {
return playerNumber || 0;
};
@@ -210,6 +229,14 @@ namespace gdjs {
: `Player ${playerNumber}`;
};
/**
* Returns the player username of the current player in the lobby.
*/
export const getCurrentPlayerUsername = () => {
const currentPlayerNumber = getCurrentPlayerNumber();
return getPlayerUsername(currentPlayerNumber);
};
const handleLeavingPlayer = (runtimeScene: gdjs.RuntimeScene) => {
const disconnectedPlayers = gdjs.multiplayerMessageManager.getDisconnectedPlayers();
if (disconnectedPlayers.length > 0) {
@@ -225,7 +252,7 @@ namespace gdjs {
);
if (playerLeftPublicProfile) {
multiplayerComponents.displayPlayerLeftNotification(
gdjs.multiplayerComponents.displayPlayerLeftNotification(
runtimeScene,
(playerLeftPublicProfile && playerLeftPublicProfile.username) ||
'Player'
@@ -371,7 +398,6 @@ namespace gdjs {
// Register a heartbeat to keep the connection alive.
_websocketHeartbeatInterval = setInterval(() => {
if (_websocket) {
logger.info('Heartbeat sent to keep connection alive.');
_websocket.send(
JSON.stringify({
action: 'heartbeat',
@@ -381,9 +407,22 @@ namespace gdjs {
}
}, DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL);
// When socket is open, ask for the connectionId, so that we can inform the lobbies window.
// When socket is open, ask for the connectionId and send more session info, so that we can inform the lobbies window.
if (_websocket) {
_websocket.send(JSON.stringify({ action: 'getConnectionId' }));
const platformInfo = runtimeScene.getGame().getPlatformInfo();
_websocket.send(
JSON.stringify({
action: 'sessionInformation',
connectionType: 'lobby',
isCordova: platformInfo.isCordova,
devicePlatform: platformInfo.devicePlatform,
navigatorPlatform: platformInfo.navigatorPlatform,
hasTouch: platformInfo.hasTouch,
supportedCompressionMethods:
platformInfo.supportedCompressionMethods,
})
);
}
};
_websocket.onmessage = (event) => {
@@ -399,6 +438,11 @@ namespace gdjs {
if (!connectionId || !positionInLobby) {
logger.error('No connectionId or position received');
gdjs.multiplayerComponents.displayErrorNotification(
runtimeScene
);
// Close the websocket as something wrong happened.
if (_websocket) _websocket.close();
return;
}
@@ -422,11 +466,24 @@ namespace gdjs {
logger.error('No lobby received');
return;
}
handleLobbyUpdatedEvent(runtimeScene, lobby, positionInLobby);
handleLobbyUpdatedEvent({
runtimeScene,
updatedLobby: lobby,
positionInLobby,
});
break;
}
case 'gameCountdownStarted': {
handleGameCountdownStartedEvent(runtimeScene);
const messageData = messageContent.data;
const compressionMethod = messageData.compressionMethod || 'none';
const secondsToStart =
messageData.secondsToStart ||
DEFAULT_COUNTDOWN_SECONDS_TO_START;
handleGameCountdownStartedEvent({
runtimeScene,
compressionMethod,
secondsToStart,
});
break;
}
case 'gameStarted': {
@@ -435,7 +492,7 @@ namespace gdjs {
messageData.heartbeatInterval ||
DEFAULT_LOBBY_HEARTBEAT_INTERVAL;
handleGameStartedEvent(runtimeScene, heartbeatInterval);
handleGameStartedEvent({ runtimeScene, heartbeatInterval });
break;
}
case 'peerId': {
@@ -450,7 +507,7 @@ namespace gdjs {
return;
}
handlePeerIdEvent(peerId);
handlePeerIdEvent({ peerId });
break;
}
}
@@ -473,7 +530,7 @@ namespace gdjs {
return;
}
const lobbiesIframe = multiplayerComponents.getLobbiesIframe(
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
@@ -523,7 +580,7 @@ namespace gdjs {
// When the connectionId is received, initialise PeerJS so players can connect to each others afterwards.
if (validIceServers.length) {
for (const server of validIceServers) {
gdjs.evtTools.p2p.useCustomICECandidate(
gdjs.multiplayerPeerJsHelper.useCustomICECandidate(
server.urls,
server.username,
server.credential
@@ -531,7 +588,7 @@ namespace gdjs {
}
}
if (brokerServerConfig) {
gdjs.evtTools.p2p.useCustomBrokerServer(
gdjs.multiplayerPeerJsHelper.useCustomBrokerServer(
brokerServerConfig.hostname,
brokerServerConfig.port,
brokerServerConfig.path,
@@ -539,7 +596,7 @@ namespace gdjs {
brokerServerConfig.secure
);
} else {
gdjs.evtTools.p2p.useDefaultBrokerServer();
gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();
}
_connectionId = connectionId;
@@ -548,7 +605,7 @@ namespace gdjs {
_lobbyId = lobbyId;
// Then we inform the lobbies window that the player has joined.
const lobbiesIframe = multiplayerComponents.getLobbiesIframe(
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
@@ -570,8 +627,8 @@ namespace gdjs {
},
// Specify the origin to avoid leaking the playerToken.
// Replace with '*' to test locally.
'https://gd.games'
// '*'
// 'https://gd.games'
'*'
);
};
@@ -586,11 +643,15 @@ namespace gdjs {
_websocket = null;
};
const handleLobbyUpdatedEvent = function (
runtimeScene: gdjs.RuntimeScene,
const handleLobbyUpdatedEvent = function ({
runtimeScene,
updatedLobby,
positionInLobby: number
) {
positionInLobby,
}: {
runtimeScene: gdjs.RuntimeScene;
updatedLobby: Lobby;
positionInLobby: number;
}) {
// Update the object representing the lobby in the extension.
_lobby = updatedLobby;
@@ -606,7 +667,7 @@ namespace gdjs {
// If the player is in the lobby, tell the lobbies window that the lobby has been updated,
// as well as the player position.
const lobbiesIframe = multiplayerComponents.getLobbiesIframe(
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
@@ -624,16 +685,24 @@ namespace gdjs {
);
};
const handleGameCountdownStartedEvent = function (
runtimeScene: gdjs.RuntimeScene
) {
const handleGameCountdownStartedEvent = function ({
runtimeScene,
compressionMethod,
secondsToStart,
}: {
runtimeScene: gdjs.RuntimeScene;
compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;
secondsToStart: number;
}) {
gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);
// When the countdown starts, if we are player number 1, then send the peerId to others so they can connect via P2P.
if (getPlayerNumber() === 1) {
if (getCurrentPlayerNumber() === 1) {
sendPeerId();
}
// Just pass along the message to the iframe so that it can display the countdown.
const lobbiesIframe = multiplayerComponents.getLobbiesIframe(
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
@@ -645,28 +714,36 @@ namespace gdjs {
lobbiesIframe.contentWindow.postMessage(
{
id: 'gameCountdownStarted',
secondsToStart,
},
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
);
// Prevent the player from leaving the lobby while the game is starting.
multiplayerComponents.hideLobbiesCloseButtonTemporarily(runtimeScene);
gdjs.multiplayerComponents.hideLobbiesCloseButtonTemporarily(
runtimeScene
);
};
/**
* When the game receives the information that the game has started, close the
* lobbies window, focus on the game, and set the flag to true.
*/
const handleGameStartedEvent = function (
runtimeScene: gdjs.RuntimeScene,
heartbeatInterval: number
) {
const handleGameStartedEvent = function ({
runtimeScene,
heartbeatInterval,
}: {
runtimeScene: gdjs.RuntimeScene;
heartbeatInterval: number;
}) {
// It is possible the connection to other players didn't work.
// If that's the case, show an error message and leave the lobby.
// If we are the host, still start the game, as this allows a player to test the game alone.
const allConnectedPeers = gdjs.evtTools.p2p.getAllPeers();
const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();
if (!isPlayerHost() && allConnectedPeers.length === 0) {
multiplayerComponents.displayConnectionErrorNotification(runtimeScene);
gdjs.multiplayerComponents.displayConnectionErrorNotification(
runtimeScene
);
// Do as if the player left the lobby.
handleLobbyLeaveEvent();
removeLobbiesContainer(runtimeScene);
@@ -732,7 +809,7 @@ namespace gdjs {
}
// Disconnect from any P2P connections.
gdjs.evtTools.p2p.disconnectFromAllPeers();
gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();
// Clear the expected acknowledgments, as the game is ending.
gdjs.multiplayerMessageManager.clearExpectedMessageAcknowledgements();
@@ -742,9 +819,9 @@ namespace gdjs {
* When the game receives the information of the peerId, then
* the player can connect to the peer.
*/
const handlePeerIdEvent = function (peerId: string) {
const handlePeerIdEvent = function ({ peerId }: { peerId: string }) {
// When a peerId is received, trigger a P2P connection with the peer.
const currentPeerId = gdjs.evtTools.p2p.getCurrentId();
const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
if (!currentPeerId) {
logger.error(
'No peerId found, the player does not seem connected to the broker server.'
@@ -757,7 +834,7 @@ namespace gdjs {
return;
}
gdjs.evtTools.p2p.connect(peerId);
gdjs.multiplayerPeerJsHelper.connect(peerId);
};
/**
@@ -819,6 +896,8 @@ namespace gdjs {
// Consider the game is ended, so that we don't listen to other players disconnecting.
_isLobbyGameRunning = false;
logger.info('Ending the lobby game.');
// Inform the players that the game has ended.
gdjs.multiplayerMessageManager.sendEndGameMessage();
@@ -869,7 +948,7 @@ namespace gdjs {
return;
}
const peerId = gdjs.evtTools.p2p.getCurrentId();
const peerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
if (!peerId) {
logger.error(
"No peerId found, the player doesn't seem connected to the broker server."
@@ -909,6 +988,10 @@ namespace gdjs {
// Handle message.
switch (event.data.id) {
case 'lobbiesListenerReady': {
sendSessionInformation(runtimeScene);
break;
}
case 'joinLobby': {
if (!event.data.lobbyId) {
throw new Error('Malformed message.');
@@ -944,6 +1027,29 @@ namespace gdjs {
focusOnGame(runtimeScene);
};
const sendSessionInformation = (runtimeScene: gdjs.RuntimeScene) => {
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
if (!lobbiesIframe || !lobbiesIframe.contentWindow) {
// Cannot send the message if the iframe is not opened.
return;
}
const platformInfo = runtimeScene.getGame().getPlatformInfo();
lobbiesIframe.contentWindow.postMessage(
{
id: 'sessionInformation',
isCordova: platformInfo.isCordova,
devicePlatform: platformInfo.devicePlatform,
navigatorPlatform: platformInfo.navigatorPlatform,
hasTouch: platformInfo.hasTouch,
},
'*'
);
};
/**
* Helper to handle lobbies iframe.
* We open an iframe, and listen to messages posted back to the game window.
@@ -966,7 +1072,7 @@ namespace gdjs {
};
window.addEventListener('message', _lobbiesMessageCallback, true);
multiplayerComponents.displayIframeInsideLobbiesContainer(
gdjs.multiplayerComponents.displayIframeInsideLobbiesContainer(
runtimeScene,
targetUrl
);
@@ -994,7 +1100,7 @@ namespace gdjs {
return;
}
if (_isCheckingIfGameIsRegistered || _isWaitingForLoginCallback) {
if (_isCheckingIfGameIsRegistered || _isWaitingForLogin) {
// The action is called multiple times, let's prevent that.
return;
}
@@ -1019,13 +1125,13 @@ namespace gdjs {
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (!playerId || !playerToken) {
_isWaitingForLoginCallback = true;
_isWaitingForLogin = true;
const {
status,
} = await gdjs.playerAuthentication.openAuthenticationWindow(
runtimeScene
).promise;
_isWaitingForLoginCallback = false;
_isWaitingForLogin = false;
if (status === 'logged') {
openLobbiesWindow(runtimeScene);
@@ -1034,7 +1140,7 @@ namespace gdjs {
return;
}
multiplayerComponents.displayLobbies(
gdjs.multiplayerComponents.displayLobbies(
runtimeScene,
onLobbiesContainerDismissed
);
@@ -1076,7 +1182,7 @@ namespace gdjs {
'_blank'
);
multiplayerComponents.addTextsToLoadingContainer(
gdjs.multiplayerComponents.addTextsToLoadingContainer(
runtimeScene,
_isGameRegistered,
wikiOpenAction
@@ -1093,7 +1199,7 @@ namespace gdjs {
export const isLobbiesWindowOpen = function (
runtimeScene: gdjs.RuntimeScene
): boolean {
const lobbiesRootContainer = multiplayerComponents.getLobbiesRootContainer(
const lobbiesRootContainer = gdjs.multiplayerComponents.getLobbiesRootContainer(
runtimeScene
);
return !!lobbiesRootContainer;
@@ -1103,7 +1209,7 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene,
visible: boolean
) {
multiplayerComponents.changeLobbiesWindowCloseActionVisibility(
gdjs.multiplayerComponents.changeLobbiesWindowCloseActionVisibility(
runtimeScene,
visible
);
@@ -1116,7 +1222,7 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene
) {
removeLobbiesCallbacks();
multiplayerComponents.removeLobbiesContainer(runtimeScene);
gdjs.multiplayerComponents.removeLobbiesContainer(runtimeScene);
};
/*
@@ -1137,5 +1243,15 @@ namespace gdjs {
const gameCanvas = runtimeScene.getGame().getRenderer().getCanvas();
if (gameCanvas) gameCanvas.focus();
};
/**
* Action to allow the player to leave the lobby in-game.
*/
export const leaveGameLobby = async (runtimeScene: gdjs.RuntimeScene) => {
// Handle the case where the game has not started yet, so the player is in the lobby.
handleLobbyLeaveEvent();
// Handle the case where the game has started, so the player is in the game and connected to other players.
handleLobbyGameEnded();
};
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,454 @@
/// <reference path="peerjs.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
export namespace multiplayerPeerJsHelper {
/**
* The type of the data that is sent across peerjs.
* We use UInt8Array to send compressed data, but we only manipulate objects once received.
*/
type NetworkMessage = {
messageName: string;
data: Uint8Array | string;
};
export type CompressionMethod = 'none' | 'cs:gzip' | 'cs:deflate';
/**
* Helper to discard invalid messages when received.
*/
const isValidNetworkMessage = (
message: unknown
): message is NetworkMessage =>
typeof message === 'object' &&
message !== null &&
typeof message['messageName'] === 'string' &&
typeof message['data'] === 'object';
export interface IMessageData {
readonly data: any; // The data sent with the message, an object with unknown content.
readonly sender: String;
getData(): any;
getSender(): string;
}
/**
* The data bound to a message name.
*/
export class MessageData implements IMessageData {
public readonly data: any;
public readonly sender: string;
constructor(data: object, sender: string) {
this.data = data;
this.sender = sender;
}
public getData(): any {
return this.data;
}
public getSender(): string {
return this.sender;
}
}
export interface IMessagesList {
getName(): string;
getMessages(): IMessageData[];
pushMessage(data: object, sender: string): void;
}
export class MessagesList implements IMessagesList {
private readonly data: IMessageData[] = [];
private readonly messageName: string;
constructor(messageName: string) {
this.messageName = messageName;
}
public getName(): string {
return this.messageName;
}
public getMessages(): IMessageData[] {
return this.data;
}
public pushMessage(data: object, sender: string): void {
this.data.push(new MessageData(data, sender));
}
}
/**
* The peer to peer configuration.
*/
let peerConfig: Peer.PeerJSOption = { debug: 1 };
/**
* The p2p client.
*/
let peer: Peer<NetworkMessage> | null = null;
/**
* All connected p2p clients, keyed by their ID.
*/
const connections = new Map<string, Peer.DataConnection<NetworkMessage>>();
/**
* Contains a map of message triggered by other p2p clients.
* It is keyed by the event name.
*/
const allMessages = new Map<string, IMessagesList>();
/**
* True if PeerJS is initialized and ready.
*/
let ready = false;
/**
* List of IDs of peers that just disconnected.
*/
const justDisconnectedPeers: string[] = [];
/**
* List of IDs of peers that just remotely initiated a connection.
*/
const justConnectedPeers: string[] = [];
/**
* The compression method used to compress data sent over the network.
*/
let compressionMethod: CompressionMethod = 'none';
export const setCompressionMethod = (method: CompressionMethod) => {
compressionMethod = method;
};
/**
* Helper function to compress data sent over the network.
*/
async function compressData(data: object): Promise<Uint8Array | string> {
if (compressionMethod === 'none') {
// If no compression is used, we just stringify the data,
// PeerJS will compress it to binary data.
const jsonString = JSON.stringify(data);
return jsonString;
}
const compressionStreamFormat =
compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';
const jsonString = JSON.stringify(data);
const encoder = new TextEncoder();
const array = encoder.encode(jsonString);
// @ts-ignore - We checked that CompressionStream is available in the browser.
const cs = new CompressionStream(compressionStreamFormat);
const writer = cs.writable.getWriter();
writer.write(array);
writer.close();
const compressedStream = cs.readable;
const reader = compressedStream.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const compressedData = new Uint8Array(
chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])
);
return compressedData;
}
/**
* Helper function to decompress data received over the network.
* It returns the parsed JSON object, if valid, or undefined.
*/
async function decompressData(
receivedData: Uint8Array | string
): Promise<object | undefined> {
if (compressionMethod === 'none') {
// If no compression is used, we just parse the data.
if (typeof receivedData !== 'string') {
logger.error(
`Error while parsing message using compressionMethod ${compressionMethod}: received data is not a string.`
);
return;
}
try {
const parsedData = JSON.parse(receivedData);
return parsedData;
} catch (e) {
logger.error(`Error while parsing message: ${e.toString()}`);
return;
}
}
const compressionStreamFormat =
compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';
// @ts-ignore - We checked that DecompressionStream is available in the browser.
const ds = new DecompressionStream(compressionStreamFormat);
const writer = ds.writable.getWriter();
writer.write(receivedData);
writer.close();
const decompressedStream = ds.readable;
const reader = decompressedStream.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const decompressedData = new Uint8Array(
chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])
);
const decoder = new TextDecoder();
const jsonStringData = decoder.decode(decompressedData); // Convert Uint8Array back to string
try {
const parsedData = JSON.parse(jsonStringData);
return parsedData;
} catch (e) {
logger.error(`Error while parsing message: ${e.toString()}`);
return;
}
}
/**
* Helper function to get the messages list for a given message name.
*/
export const getOrCreateMessagesList = (
messageName: string
): IMessagesList => {
const messagesList = allMessages.get(messageName);
if (messagesList) return messagesList;
const newMessagesList = new MessagesList(messageName);
allMessages.set(messageName, newMessagesList);
return newMessagesList;
};
/**
* Internal function called when a connection with a remote peer is initiated.
* @param connection The DataConnection of the peer
*/
const _onConnect = (connection: Peer.DataConnection<NetworkMessage>) => {
connections.set(connection.peer, connection);
connection.on('data', async (data) => {
if (isValidNetworkMessage(data)) {
const messagesList = getOrCreateMessagesList(data.messageName);
const messageSender = connection.peer;
const decompressedData = await decompressData(data.data);
if (!decompressedData) return;
messagesList.pushMessage(decompressedData, messageSender);
}
});
// Close event is only for graceful disconnection,
// but we want onDisconnect to trigger for any type of disconnection,
// so we register a listener for both event types.
connection.on('error', () => {
_onDisconnect(connection.peer);
});
connection.on('close', () => {
_onDisconnect(connection.peer);
});
// Regularly check for disconnection as the built in way is not reliable.
(function disconnectChecker() {
if (
connection.peerConnection &&
(connection.peerConnection.connectionState === 'failed' ||
connection.peerConnection.connectionState === 'disconnected' ||
connection.peerConnection.connectionState === 'closed')
) {
_onDisconnect(connection.peer);
} else {
setTimeout(disconnectChecker, 1000);
}
})();
};
/**
* Internal function called when a remote client disconnects.
* @param connectionID The ID of the peer that disconnected.
*/
const _onDisconnect = (connectionID: string) => {
if (!connections.has(connectionID)) return;
justDisconnectedPeers.push(connectionID);
connections.delete(connectionID);
};
/**
* Internal function called to initialize PeerJS after it
* has been configured.
*/
const loadPeerJS = () => {
if (peer !== null) return;
peer = new Peer(peerConfig);
peer.on('open', () => {
ready = true;
});
peer.on('error', (errorMessage) => {
logger.error('PeerJS error:', errorMessage);
});
peer.on('connection', (connection) => {
connection.on('open', () => {
_onConnect(connection);
justConnectedPeers.push(connection.peer);
});
});
peer.on('close', () => {
peer = null;
loadPeerJS();
});
peer.on('disconnected', peer.reconnect);
};
/**
* Connects to another p2p client.
* @param id - The other client's ID.
*/
export const connect = (id: string) => {
if (peer === null) return;
const connection = peer.connect(id);
connection.on('open', () => {
_onConnect(connection);
});
};
/**
* Disconnects from all other p2p clients.
*/
export const disconnectFromAllPeers = () => {
for (const connection of connections.values()) connection.close();
};
/**
* Send a message to a specific peer.
* @param ids - The IDs of the clients to send the event to.
* @param messageName - The event to trigger.
* @param eventData - Additional data to send with the event.
*/
export const sendDataTo = async (
ids: string[],
messageName: string,
messageData: object
) => {
if (!ids.length) return;
const compressedData = await compressData(messageData);
for (const id of ids) {
const connection = connections.get(id);
if (connection) {
connection.send({
messageName,
data: compressedData,
});
}
}
};
export const getAllMessagesMap = () => allMessages;
/**
* Connects to a custom broker server.
* @param host The host of the broker server.
* @param port The port of the broker server.
* @param path The path (part of the url after the host) to the broker server.
* @param key Optional password to connect to the broker server.
* @param ssl Use ssl?
*/
export const useCustomBrokerServer = (
host: string,
port: number,
path: string,
key: string,
ssl: boolean
) => {
Object.assign(peerConfig, {
host,
port,
path,
secure: ssl,
// All servers have "peerjs" as default key
key: key.length === 0 ? 'peerjs' : key,
});
loadPeerJS();
};
export const useDefaultBrokerServer = loadPeerJS;
/**
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
* @param urls The URL of the STUN/TURN server.
* @param username An optional username to send to the server.
* @param credential An optional password to send to the server.
*/
export const useCustomICECandidate = (
urls: string,
username?: string,
credential?: string
) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
peerConfig.config.iceServers.push({
urls,
username,
credential,
});
};
/**
* Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.
* @param shouldUseRelayServer Whether relay-only should be enabled or disabled.
*/
export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceTransportPolicy = shouldUseRelayServer
? 'relay'
: 'all';
};
/**
* Returns the own current peer ID.
* @see Peer.id
*/
export const getCurrentId = (): string => {
if (peer == undefined) return '';
return peer.id || '';
};
/**
* Returns true once PeerJS finished initialization.
* @see ready
*/
export const isReady = () => ready;
/**
* Return any disconnected peers.
*/
export const getJustDisconnectedPeers = () => justDisconnectedPeers;
/**
* Returns the list of all currently connected peers.
*/
export const getAllPeers = () => Array.from(connections.keys());
gdjs.callbacksRuntimeScenePostEvents.push(() => {
// Clear the list of messages at the end of the frame, assuming they've been all processed.
for (const messagesList of allMessages.values()) {
messagesList.getMessages().length = 0;
}
// Clear the list of just connected and disconnected peers.
if (justDisconnectedPeers.length > 0) {
justDisconnectedPeers.length = 0;
}
if (justConnectedPeers.length > 0) {
justConnectedPeers.length = 0;
}
});
}
}

509
Extensions/Multiplayer/peerjs.d.ts vendored Normal file
View File

@@ -0,0 +1,509 @@
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*/
declare class EventEmitter<
EventTypes extends EventEmitter.ValidEventTypes = string | symbol,
Context extends any = any
> {
static prefixed: string | boolean;
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*/
eventNames(): Array<EventEmitter.EventNames<EventTypes>>;
/**
* Return the listeners registered for a given event.
*/
listeners<T extends EventEmitter.EventNames<EventTypes>>(
event: T
): Array<EventEmitter.EventListener<EventTypes, T>>;
/**
* Return the number of listeners listening to a given event.
*/
listenerCount(event: EventEmitter.EventNames<EventTypes>): number;
/**
* Calls each of the listeners registered for a given event.
*/
emit<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
...args: EventEmitter.EventArgs<EventTypes, T>
): boolean;
/**
* Add a listener for a given event.
*/
on<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
addListener<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
/**
* Add a one-time listener for a given event.
*/
once<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
/**
* Remove the listeners of a given event.
*/
removeListener<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn?: EventEmitter.EventListener<EventTypes, T>,
context?: Context,
once?: boolean
): this;
off<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn?: EventEmitter.EventListener<EventTypes, T>,
context?: Context,
once?: boolean
): this;
/**
* Remove all listeners, or those of the specified event.
*/
removeAllListeners(event?: EventEmitter.EventNames<EventTypes>): this;
}
declare namespace EventEmitter {
export interface ListenerFn<Args extends any[] = any[]> {
(...args: Args): void;
}
export interface EventEmitterStatic {
new <
EventTypes extends ValidEventTypes = string | symbol,
Context = any
>(): EventEmitter<EventTypes, Context>;
}
/**
* `object` should be in either of the following forms:
* ```
* interface EventTypes {
* 'event-with-parameters': any[]
* 'event-with-example-handler': (...args: any[]) => void
* }
* ```
*/
export type ValidEventTypes = string | symbol | object;
export type EventNames<T extends ValidEventTypes> = T extends string | symbol
? T
: keyof T;
export type ArgumentMap<T extends object> = {
[K in keyof T]: T[K] extends (...args: any[]) => void
? Parameters<T[K]>
: T[K] extends any[]
? T[K]
: any[];
};
export type EventListener<
T extends ValidEventTypes,
K extends EventNames<T>
> = T extends string | symbol
? (...args: any[]) => void
: (
...args: ArgumentMap<Exclude<T, string | symbol>>[Extract<K, keyof T>]
) => void;
export type EventArgs<
T extends ValidEventTypes,
K extends EventNames<T>
> = Parameters<EventListener<T, K>>;
export const EventEmitter: EventEmitterStatic;
}
declare namespace Peer {
export interface UtilSupportsObj {
browser: boolean;
webRTC: boolean;
audioVideo: boolean;
data: boolean;
binaryBlob: boolean;
reliable: boolean;
}
class Util {
noop(): void;
readonly CLOUD_HOST = '0.peerjs.com';
readonly CLOUD_PORT = 443;
readonly chunkedBrowsers: {
Chrome: number;
chrome: number;
};
readonly chunkedMTU = 16300;
readonly defaultConfig: {
iceServers: (
| {
urls: string;
username?: undefined;
credential?: undefined;
}
| {
urls: string[];
username: string;
credential: string;
}
)[];
sdpSemantics: string;
};
readonly browser: string;
readonly browserVersion: number;
readonly supports: UtilSupportsObj;
validateId(id: string): boolean;
pack: any;
unpack: any;
chunk(
blob: Blob
): {
__peerData: number;
n: number;
total: number;
data: Blob;
}[];
blobToArrayBuffer(
blob: Blob,
cb: (arg: ArrayBuffer | null) => void
): FileReader;
binaryStringToArrayBuffer(binary: string): ArrayBuffer | SharedArrayBuffer;
randomToken(): string;
isSecure(): boolean;
}
export const util: Util;
export enum LogLevel {
Disabled = 0,
Errors = 1,
Warnings = 2,
All = 3,
}
export enum ConnectionType {
Data = 'data',
Media = 'media',
}
export enum PeerErrorType {
BrowserIncompatible = 'browser-incompatible',
Disconnected = 'disconnected',
InvalidID = 'invalid-id',
InvalidKey = 'invalid-key',
Network = 'network',
PeerUnavailable = 'peer-unavailable',
SslUnavailable = 'ssl-unavailable',
ServerError = 'server-error',
SocketError = 'socket-error',
SocketClosed = 'socket-closed',
UnavailableID = 'unavailable-id',
WebRTC = 'webrtc',
}
export enum SerializationType {
Binary = 'binary',
BinaryUTF8 = 'binary-utf8',
JSON = 'json',
}
export enum SocketEventType {
Message = 'message',
Disconnected = 'disconnected',
Error = 'error',
Close = 'close',
}
export enum ServerMessageType {
Heartbeat = 'HEARTBEAT',
Candidate = 'CANDIDATE',
Offer = 'OFFER',
Answer = 'ANSWER',
Open = 'OPEN',
Error = 'ERROR',
IdTaken = 'ID-TAKEN',
InvalidKey = 'INVALID-KEY',
Leave = 'LEAVE',
Expire = 'EXPIRE',
}
/**
* An abstraction on top of WebSockets to provide fastest
* possible connection for peers.
*/
class Socket extends EventEmitter {
constructor(
secure: any,
host: string,
port: number,
path: string,
key: string,
pingInterval?: number
);
start(id: string, token: string): void;
/** Exposed send for DC & Peer. */
send(data: any): void;
close(): void;
}
class ServerMessage {
type: ServerMessageType;
payload: any;
src: string;
}
type BaseConnectionEvents = {
/**
* Emitted when either you or the remote peer closes the connection.
*/
close: () => void;
error: (error: Error) => void;
iceStateChanged: (state: RTCIceConnectionState) => void;
};
abstract class BaseConnection<
T extends EventEmitter.ValidEventTypes,
TT
> extends EventEmitter<T & BaseConnectionEvents> {
readonly peer: string;
provider: Peer<TT>;
readonly options: any;
protected _open: boolean;
readonly metadata: any;
connectionId: string;
peerConnection: RTCPeerConnection;
abstract get type(): ConnectionType;
get open(): boolean;
constructor(peer: string, provider: Peer<TT>, options: any);
abstract close(): void;
abstract handleMessage(message: ServerMessage): void;
}
type DataConnectionEvents<T> = {
/**
* Emitted when data is received from the remote peer.
*/
data: (data: T) => void;
/**
* Emitted when the connection is established and ready-to-use.
*/
open: () => void;
};
/**
* Wraps a DataChannel between two Peers.
*/
export class DataConnection<T> extends BaseConnection<
DataConnectionEvents<T>,
T
> {
readonly label: string;
readonly serialization: SerializationType;
readonly reliable: boolean;
stringify: (data: any) => string;
parse: (data: string) => any;
get type(): ConnectionType;
get dataChannel(): RTCDataChannel;
get bufferSize(): number;
constructor(peerId: string, provider: Peer<T>, options: any);
/** Called by the Negotiator when the DataChannel is ready. */
initialize(dc: RTCDataChannel): void;
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
close(): void;
/** Allows user to send data. */
send(data: T, chunked?: boolean): void;
handleMessage(message: ServerMessage): void;
}
export interface AnswerOption {
sdpTransform?: Function;
}
export interface PeerJSOption {
key?: string;
host?: string;
port?: number;
path?: string;
secure?: boolean;
token?: string;
config?: RTCConfiguration;
debug?: number;
referrerPolicy?: ReferrerPolicy;
}
export interface PeerConnectOption {
label?: string;
metadata?: any;
serialization?: string;
reliable?: boolean;
}
export interface CallOption {
metadata?: any;
sdpTransform?: Function;
}
type MediaConnectionEvents = {
/**
* Emitted when a connection to the PeerServer is established.
*/
stream: (stream: MediaStream) => void;
};
/**
* Wraps the streaming interface between two Peers.
*/
export class MediaConnection<T> extends BaseConnection<
MediaConnectionEvents,
T
> {
get type(): ConnectionType;
get localStream(): MediaStream;
get remoteStream(): MediaStream;
constructor(peerId: string, provider: Peer<T>, options: any);
addStream(remoteStream: any): void;
handleMessage(message: ServerMessage): void;
answer(stream?: MediaStream, options?: AnswerOption): void;
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
close(): void;
}
class PeerOptions implements PeerJSOption {
debug?: LogLevel;
host?: string;
port?: number;
path?: string;
key?: string;
token?: string;
config?: any;
secure?: boolean;
pingInterval?: number;
referrerPolicy?: ReferrerPolicy;
logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
}
type PeerEvents<T> = {
/**
* Emitted when a connection to the PeerServer is established.
*/
open: (id: string) => void;
/**
* Emitted when a new data connection is established from a remote peer.
*/
connection: (dataConnection: DataConnection<T>) => void;
/**
* Emitted when a remote peer attempts to call you.
*/
call: (mediaConnection: MediaConnection<T>) => void;
/**
* Emitted when the peer is destroyed and can no longer accept or create any new connections.
*/
close: () => void;
/**
* Emitted when the peer is disconnected from the signalling server
*/
disconnected: (currentId: string) => void;
/**
* Errors on the peer are almost always fatal and will destroy the peer.
*/
error: (error: Error) => void;
};
}
/**
* A peer who can initiate connections with other peers.
*/
declare class Peer<T> extends EventEmitter<Peer.PeerEvents<T>> {
/**
* The brokering ID of this peer
*/
get id(): string;
get options(): Peer.PeerOptions;
get open(): boolean;
get socket(): Peer.Socket;
/**
* A hash of all connections associated with this peer, keyed by the remote peer's ID.
* @deprecated
* Return type will change from Object to Map<string,[]>
*/
get connections(): Object;
/**
* true if this peer and all of its connections can no longer be used.
*/
get destroyed(): boolean;
/**
* false if there is an active connection to the PeerServer.
*/
get disconnected(): boolean;
/**
* A peer can connect to other peers and listen for connections.
*/
constructor();
/**
* A peer can connect to other peers and listen for connections.
* @param options for specifying details about PeerServer
*/
constructor(options: Peer.PeerOptions);
/**
* A peer can connect to other peers and listen for connections.
* @param id Other peers can connect to this peer using the provided ID.
* If no ID is given, one will be generated by the brokering server.
* @param options for specifying details about PeerServer
*/
constructor(id: string, options?: Peer.PeerOptions);
/** Retrieve messages from lost message store */
_getMessages(connectionId: string): Peer.ServerMessage[];
/**
* Connects to the remote peer specified by id and returns a data connection.
* @param peer The brokering ID of the remote peer (their peer.id).
* @param options for specifying details about Peer Connection
*/
connect(
peer: string,
options?: Peer.PeerConnectOption
): Peer.DataConnection<T>;
/**
* Calls the remote peer specified by id and returns a media connection.
* @param peer The brokering ID of the remote peer (their peer.id).
* @param stream The caller's media stream
* @param options Metadata associated with the connection, passed in by whoever initiated the connection.
*/
call(
peer: string,
stream: MediaStream,
options?: Peer.CallOption
): Peer.MediaConnection<T>;
_removeConnection(
connection: Peer.DataConnection<T> | Peer.MediaConnection<T>
): void;
/** Retrieve a data/media connection for this peer. */
getConnection(
peerId: string,
connectionId: string
): null | Peer.DataConnection<T> | Peer.MediaConnection<T>;
/** Emits a typed error message. */
emitError(type: Peer.PeerErrorType, err: string | Error): void;
/**
* Destroys the Peer: closes all active connections as well as the connection
* to the server.
* Warning: The peer can no longer create or accept connections after being
* destroyed.
*/
destroy(): void;
/**
* Disconnects the Peer's connection to the PeerServer. Does not close any
* active connections.
* Warning: The peer can no longer create or accept connections after being
* disconnected. It also cannot reconnect to the server.
*/
disconnect(): void;
/** Attempts to reconnect with the same ID. */
reconnect(): void;
/**
* Get a list of available peer IDs. If you're running your own server, you'll
* want to set allow_discovery: true in the PeerServer options. If you're using
* the cloud server, email team@peerjs.com to get the functionality enabled for
* your key.
*/
listAllPeers(cb?: (_: any[]) => void): void;
}

File diff suppressed because it is too large Load Diff

View File

@@ -296,8 +296,7 @@ namespace gdjs {
): boolean => {
const event = getEvent(eventName);
event.dataloss = defaultDataLoss;
const isTriggered = event.isTriggered();
return isTriggered;
return event.isTriggered();
};
/**
@@ -364,10 +363,8 @@ namespace gdjs {
* @param eventName - The event to get data from.
* @returns - The data as JSON.
*/
export const getEventData = (eventName: string) => {
const data = getEvent(eventName).getData();
return data;
};
export const getEventData = (eventName: string) =>
getEvent(eventName).getData();
/**
* Get the id of peer that caused the last trigger of an event.
@@ -388,8 +385,6 @@ namespace gdjs {
variable.fromJSON(getEventData(eventName));
};
export const getEvents = () => events;
/**
* Connects to a custom broker server.
* @param host The host of the broker server.
@@ -513,11 +508,6 @@ namespace gdjs {
*/
export const getConnectedPeer = (): string => connectedPeers[0] || '';
/**
* Returns the list of all currently connected peers.
*/
export const getAllPeers = () => Array.from(connections.keys());
/**
* A JavaScript-only function to get the raw P2P DataConnection.
* This can be useful for example when you want to use a binary protocol

View File

@@ -120,9 +120,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): PanelSpriteNetworkSyncData {
getNetworkSyncData(): PanelSpriteNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
wid: this.getWidth(),
hei: this.getHeight(),
op: this.getOpacity(),
@@ -130,10 +130,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: PanelSpriteNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -382,9 +382,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): ParticleEmitterObjectNetworkSyncData {
getNetworkSyncData(): ParticleEmitterObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
prms: this.particleRotationMinSpeed,
prmx: this.particleRotationMaxSpeed,
mpc: this.maxParticlesCount,
@@ -414,10 +414,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
syncData: ParticleEmitterObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(syncData);
super.updateFromNetworkSyncData(syncData);
if (syncData.x !== undefined) {
this.setX(syncData.x);
}

View File

@@ -4,6 +4,23 @@ Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('Pathfinding behavior');
interface PathfindingNetworkSyncDataType {
// Syncing the path should be enough to have a good prediction.
path: FloatPoint[];
pf: boolean;
sp: number;
as: number;
cs: number;
tss: number;
re: boolean;
ma: number;
}
export interface PathfindingNetworkSyncData extends BehaviorNetworkSyncData {
props: PathfindingNetworkSyncDataType;
}
/**
* PathfindingRuntimeBehavior represents a behavior allowing objects to
* follow a path computed to avoid obstacles.
@@ -117,6 +134,56 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): PathfindingNetworkSyncData {
return {
...super.getNetworkSyncData(),
props: {
path: this._path,
pf: this._pathFound,
sp: this._speed,
as: this._angularSpeed,
cs: this._currentSegment,
tss: this._totalSegmentDistance,
re: this._reachedEnd,
ma: this._movementAngle,
},
};
}
updateFromNetworkSyncData(
networkSyncData: PathfindingNetworkSyncData
): void {
super.updateFromNetworkSyncData(networkSyncData);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.path !== undefined) {
this._path = behaviorSpecificProps.path;
}
if (behaviorSpecificProps.pf !== undefined) {
this._pathFound = behaviorSpecificProps.pf;
}
if (behaviorSpecificProps.sp !== undefined) {
this._speed = behaviorSpecificProps.sp;
}
if (behaviorSpecificProps.as !== undefined) {
this._angularSpeed = behaviorSpecificProps.as;
}
if (
behaviorSpecificProps.cs !== undefined &&
behaviorSpecificProps.cs !== this._currentSegment
) {
this._currentSegment = behaviorSpecificProps.cs;
}
if (behaviorSpecificProps.tss !== undefined) {
this._totalSegmentDistance = behaviorSpecificProps.tss;
}
if (behaviorSpecificProps.re !== undefined) {
this._reachedEnd = behaviorSpecificProps.re;
}
if (behaviorSpecificProps.ma !== undefined) {
this._movementAngle = behaviorSpecificProps.ma;
}
}
setCellWidth(width: float): void {
this._cellWidth = width;
}
@@ -407,7 +474,7 @@ namespace gdjs {
return;
}
//Not path found
// No path found
this._pathFound = false;
}

View File

@@ -7,6 +7,19 @@ namespace gdjs {
export interface RuntimeScene {
physics2SharedData: gdjs.Physics2SharedData | null;
}
interface Physics2NetworkSyncDataType {
tpx: number | undefined;
tpy: number | undefined;
tqa: number | undefined;
lvx: number | undefined;
lvy: number | undefined;
av: number | undefined;
aw: boolean | undefined;
}
export interface Physics2NetworkSyncData extends BehaviorNetworkSyncData {
props: Physics2NetworkSyncDataType;
}
export class Physics2SharedData {
gravityX: float;
gravityY: float;
@@ -476,41 +489,72 @@ namespace gdjs {
return true;
}
getNetworkSyncData() {
getNetworkSyncData(): Physics2NetworkSyncData {
const bodyProps = this._body
? {
tpx: this._body.GetTransform().get_p().get_x(),
tpy: this._body.GetTransform().get_p().get_y(),
tqa: this._body.GetTransform().get_q().GetAngle(),
lvx: this._body.GetLinearVelocity().get_x(),
lvy: this._body.GetLinearVelocity().get_y(),
av: this._body.GetAngularVelocity(),
aw: this._body.IsAwake(),
}
: {
tpx: undefined,
tpy: undefined,
tqa: undefined,
lvx: undefined,
lvy: undefined,
av: undefined,
aw: undefined,
};
return {
...super.getNetworkSyncData(),
props: {
oox: this._objectOldX,
ooy: this._objectOldY,
ooa: this._objectOldAngle,
oow: this._objectOldWidth,
ooh: this._objectOldHeight,
vb: this._verticesBuffer,
...bodyProps,
},
};
}
updateFromNetworkSyncData(networkSyncData: any) {
updateFromNetworkSyncData(networkSyncData: Physics2NetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.oox !== undefined) {
this._objectOldX = behaviorSpecificProps.oox;
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.ooy !== undefined) {
this._objectOldY = behaviorSpecificProps.ooy;
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined
) {
if (this._body) {
this._body.SetLinearVelocity(
this.b2Vec2(behaviorSpecificProps.lvx, behaviorSpecificProps.lvy)
);
}
}
if (behaviorSpecificProps.ooa !== undefined) {
this._objectOldAngle = behaviorSpecificProps.ooa;
if (behaviorSpecificProps.av !== undefined) {
if (this._body) {
this._body.SetAngularVelocity(behaviorSpecificProps.av);
}
}
if (behaviorSpecificProps.oow !== undefined) {
this._objectOldWidth = behaviorSpecificProps.oow;
}
if (behaviorSpecificProps.ooh !== undefined) {
this._objectOldHeight = behaviorSpecificProps.ooh;
}
if (behaviorSpecificProps.vb !== undefined) {
this._verticesBuffer = behaviorSpecificProps.vb;
if (behaviorSpecificProps.aw !== undefined) {
if (this._body) {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
}

View File

@@ -855,9 +855,9 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
"CppPlatform/Extensions/platformicon.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "PlatformBehavior")
.AddParameter(
"string",
_("Platform type (\"Platform\", \"Jumpthru\" or \"Ladder\")"))
.AddParameter("stringWithSelector",
_("Platform type"),
"[\"Platform\",\"Jumpthru\",\"Ladder\"]")
.MarkAsAdvanced()
.SetFunctionName("ChangePlatformType");
}

View File

@@ -11,6 +11,60 @@ namespace gdjs {
isCollidingAnyPlatform: boolean;
};
interface OnFloorStateNetworkSyncData {
flx: number;
fly: number;
oh: number;
}
interface FallingStateNetworkSyncData {}
interface JumpingStateNetworkSyncData {
cjs: number;
tscjs: number;
jkhsjs: boolean;
jfd: boolean;
}
interface GrabbingPlatformStateNetworkSyncData {
gplx: float;
gply: float;
}
interface OnLadderStateNetworkSyncData {}
type StateNetworkSyncData =
| OnFloorStateNetworkSyncData
| FallingStateNetworkSyncData
| JumpingStateNetworkSyncData
| GrabbingPlatformStateNetworkSyncData
| OnLadderStateNetworkSyncData;
interface PlatformerObjectNetworkSyncDataType {
cs: float;
rdx: float;
rdy: float;
ldy: float;
cfs: float;
cj: boolean;
ldl: boolean;
lek: boolean;
rik: boolean;
lak: boolean;
upk: boolean;
dok: boolean;
juk: boolean;
rpk: boolean;
rlk: boolean;
sn: string;
ssd: StateNetworkSyncData;
}
export interface PlatformerObjectNetworkSyncData
extends BehaviorNetworkSyncData {
props: PlatformerObjectNetworkSyncDataType;
}
/**
* PlatformerObjectRuntimeBehavior represents a behavior allowing objects to be
* considered as a platform by objects having PlatformerObject Behavior.
@@ -90,6 +144,10 @@ namespace gdjs {
// 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;
// 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.
_ignoreDefaultControlsAsSyncedByNetwork: boolean = false;
// This is useful for extensions that need to know
// which keys were pressed and doesn't know the mapping
@@ -161,10 +219,11 @@ namespace gdjs {
this._state = this._falling;
}
getNetworkSyncData() {
getNetworkSyncData(): 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._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
@@ -190,7 +249,9 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData) {
updateFromNetworkSyncData(
networkSyncData: PlatformerObjectNetworkSyncData
) {
super.updateFromNetworkSyncData(networkSyncData);
const behaviorSpecificProps = networkSyncData.props;
@@ -272,7 +333,7 @@ namespace gdjs {
// 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._ignoreDefaultControls = true;
this._ignoreDefaultControlsAsSyncedByNetwork = true;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -347,32 +408,38 @@ namespace gdjs {
const inputManager = instanceContainer.getGame().getInputManager();
this._leftKey ||
(this._leftKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(LEFTKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(LEFTKEY));
this._rightKey ||
(this._rightKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(RIGHTKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(RIGHTKEY));
this._jumpKey ||
(this._jumpKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
(inputManager.isKeyPressed(LSHIFTKEY) ||
inputManager.isKeyPressed(RSHIFTKEY) ||
inputManager.isKeyPressed(SPACEKEY)));
this._ladderKey ||
(this._ladderKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(UPKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(UPKEY));
this._upKey ||
(this._upKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(UPKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(UPKEY));
this._downKey ||
(this._downKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(DOWNKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(DOWNKEY));
this._releasePlatformKey ||
(this._releasePlatformKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(DOWNKEY));
!this.shouldIgnoreDefaultControls() &&
inputManager.isKeyPressed(DOWNKEY));
this._requestedDeltaX += this._updateSpeed(timeDelta);
@@ -1580,6 +1647,17 @@ namespace gdjs {
this._ignoreDefaultControls = ignore;
}
/**
* Check if the default controls of the Platformer Object are ignored.
* @returns true if the default controls are ignored.
*/
shouldIgnoreDefaultControls() {
return (
this._ignoreDefaultControls ||
this._ignoreDefaultControlsAsSyncedByNetwork
);
}
/**
* Simulate the "Left" control of the Platformer Object.
*/
@@ -1776,9 +1854,9 @@ namespace gdjs {
*/
beforeMovingY(timeDelta: float, oldX: float): void;
getNetworkSyncData(): any;
getNetworkSyncData(): StateNetworkSyncData;
updateFromNetworkSyncData(syncData: any): void;
updateFromNetworkSyncData(syncData: StateNetworkSyncData): void;
}
/**
@@ -2059,7 +2137,7 @@ namespace gdjs {
}
}
getNetworkSyncData(): any {
getNetworkSyncData(): OnFloorStateNetworkSyncData {
return {
flx: this._floorLastX,
fly: this._floorLastY,
@@ -2067,7 +2145,7 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(data: any) {
updateFromNetworkSyncData(data: OnFloorStateNetworkSyncData) {
this._floorLastX = data.flx;
this._floorLastY = data.fly;
this._oldHeight = data.oh;
@@ -2130,11 +2208,11 @@ namespace gdjs {
this._behavior._fall(timeDelta);
}
getNetworkSyncData(): any {
getNetworkSyncData(): FallingStateNetworkSyncData {
return {};
}
updateFromNetworkSyncData(data: any) {}
updateFromNetworkSyncData(data: FallingStateNetworkSyncData) {}
toString(): String {
return 'Falling';
@@ -2249,7 +2327,7 @@ namespace gdjs {
}
}
getNetworkSyncData(): any {
getNetworkSyncData(): JumpingStateNetworkSyncData {
return {
cjs: this._currentJumpSpeed,
tscjs: this._timeSinceCurrentJumpStart,
@@ -2258,7 +2336,7 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(data: any) {
updateFromNetworkSyncData(data: JumpingStateNetworkSyncData) {
this._currentJumpSpeed = data.cjs;
this._timeSinceCurrentJumpStart = data.tscjs;
this._jumpKeyHeldSinceJumpStart = data.jkhsjs;
@@ -2337,14 +2415,14 @@ namespace gdjs {
this._grabbedPlatformLastY = this._grabbedPlatform.owner.getY();
}
getNetworkSyncData(): any {
getNetworkSyncData(): GrabbingPlatformStateNetworkSyncData {
return {
gplx: this._grabbedPlatformLastX,
gply: this._grabbedPlatformLastY,
};
}
updateFromNetworkSyncData(data: any) {
updateFromNetworkSyncData(data: GrabbingPlatformStateNetworkSyncData) {
this._grabbedPlatformLastX = data.gplx;
this._grabbedPlatformLastY = data.gply;
}
@@ -2405,11 +2483,11 @@ namespace gdjs {
}
}
getNetworkSyncData(): any {
getNetworkSyncData(): OnLadderStateNetworkSyncData {
return {};
}
updateFromNetworkSyncData(data: any) {}
updateFromNetworkSyncData(data: OnLadderStateNetworkSyncData) {}
toString(): String {
return 'OnLadder';

View File

@@ -33,9 +33,9 @@ module.exports = {
extension
.addDependency()
.setName('InAppBrowser Cordova plugin')
.setName('Safari View Controller Cordova plugin')
.setDependencyType('cordova')
.setExportName('cordova-plugin-inappbrowser');
.setExportName('cordova-plugin-safariviewcontroller');
extension
.addAction(

View File

@@ -5,23 +5,18 @@ namespace gdjs {
platform,
isGameRegistered,
}: {
platform:
| 'cordova'
| 'cordova-websocket'
| 'electron'
| 'web-iframe'
| 'web';
platform: 'cordova-websocket' | 'electron' | 'web-iframe' | 'web';
isGameRegistered: boolean;
}) =>
isGameRegistered
? {
title: 'Logging in...',
text1:
platform === 'cordova' || platform === 'cordova-websocket'
platform === 'cordova-websocket'
? "One moment, we're opening a window for you to log in."
: "One moment, we're opening a new page with your web browser for you to log in.",
text2:
platform === 'cordova' || platform === 'cordova-websocket'
platform === 'cordova-websocket'
? ''
: 'If the window did not open, please check your pop-up blocker and click the button below to try again.',
}
@@ -173,12 +168,7 @@ namespace gdjs {
*/
export const addAuthenticationTextsToLoadingContainer = (
loaderContainer: HTMLDivElement,
platform:
| 'cordova'
| 'cordova-websocket'
| 'electron'
| 'web-iframe'
| 'web',
platform: 'cordova-websocket' | 'electron' | 'web-iframe' | 'web',
isGameRegistered: boolean,
wikiOpenAction: (() => void) | null
) => {

View File

@@ -1,5 +1,6 @@
namespace gdjs {
declare var cordova: any;
declare var SafariViewController: any;
const logger = new gdjs.Logger('Player Authentication');
const authComponents = gdjs.playerAuthenticationComponents;
@@ -15,16 +16,18 @@ namespace gdjs {
// Authentication display
let _authenticationWindow: Window | null = null; // For Web.
let _authenticationInAppWindow: any | null = null; // For Cordova.
let _authenticationRootContainer: HTMLDivElement | null = null;
let _authenticationLoaderContainer: HTMLDivElement | null = null;
let _authenticationIframeContainer: HTMLDivElement | null = null;
let _authenticationTextContainer: HTMLDivElement | null = null;
let _authenticationBanner: HTMLDivElement | null = null;
let _initialAuthenticationTimeoutId: NodeJS.Timeout | null = null;
let _automaticGamesPlatformAuthenticationTimeoutId: NodeJS.Timeout | null = null;
let _authenticationTimeoutId: NodeJS.Timeout | null = null;
// Communication methods.
let _automaticGamesPlatformAuthenticationCallback:
| ((event: MessageEvent) => void)
| null = null;
let _authenticationMessageCallback:
| ((event: MessageEvent) => void)
| null = null;
@@ -32,6 +35,68 @@ namespace gdjs {
type AuthenticationWindowStatus = 'logged' | 'errored' | 'dismissed';
const handleAutomaticGamesPlatformAuthentication = (
runtimeScene: gdjs.RuntimeScene
) => {
if (getPlayerAuthPlatform(runtimeScene) !== 'web') {
// Automatic authentication is only valid when the game is hosted on GDevelop games platform.
return;
}
removeAutomaticGamesPlatformAuthenticationCallback(); // Remove any callback that could have been registered before.
_automaticGamesPlatformAuthenticationCallback = (event: MessageEvent) => {
receiveAuthenticationMessage({
runtimeScene,
event,
checkOrigin: true,
});
};
window.addEventListener(
'message',
_automaticGamesPlatformAuthenticationCallback,
true
);
logger.info(
'Notifying parent window that player authentication is ready.'
);
window.parent.postMessage(
{
id: 'playerAuthReady',
},
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
);
// If no answer after 3 seconds, assume that the game is not embedded in GDevelop games platform, and remove the listener.
_automaticGamesPlatformAuthenticationTimeoutId = setTimeout(() => {
logger.info(
'Removing automatic games platform authentication listener.'
);
removeAutomaticGamesPlatformAuthenticationCallback();
}, 3000);
};
const handleAutomaticPreviewAuthentication = (
runtimeScene: gdjs.RuntimeScene
) => {
const runtimeGameOptions = runtimeScene.getGame().getAdditionalOptions();
if (runtimeGameOptions && runtimeGameOptions.isPreview) {
// If the game is a preview, and the user is already authenticated, we can log them in automatically.
const playerId = runtimeGameOptions.playerId;
const playerToken = runtimeGameOptions.playerToken;
const playerUsername = runtimeGameOptions.playerUsername;
if (playerId && playerToken) {
logger.info(
`Automatically logging in the player with ID ${playerId} as it's a preview.`
);
saveAuthKeyToStorage({
userId: playerId,
username: playerUsername || null,
userToken: playerToken,
});
refreshAuthenticationBannerIfAny(runtimeScene);
}
}
};
// Ensure that the condition "just logged in" is valid only for one frame.
gdjs.registerRuntimeScenePostEventsCallback(() => {
_justLoggedIn = false;
@@ -42,37 +107,8 @@ namespace gdjs {
// Then send a message to the parent iframe to say that the player auth is ready.
gdjs.registerFirstRuntimeSceneLoadedCallback(
(runtimeScene: RuntimeScene) => {
if (getPlayerAuthPlatform(runtimeScene) !== 'web') {
// Automatic authentication is only valid when the game is hosted on GDevelop games platform.
return;
}
removeAuthenticationCallbacks(); // Remove any callback that could have been registered before.
_authenticationMessageCallback = (event: MessageEvent) => {
receiveAuthenticationMessage({
runtimeScene,
event,
checkOrigin: true,
});
};
window.addEventListener(
'message',
_authenticationMessageCallback,
true
);
logger.info(
'Notifying parent window that player authentication is ready.'
);
window.parent.postMessage(
{
id: 'playerAuthReady',
},
'*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.
);
// If no answer after 3 seconds, assume that the game is not embedded in GDevelop games platform, and remove the listener.
_initialAuthenticationTimeoutId = setTimeout(() => {
logger.info('Removing initial authentication listener.');
removeAuthenticationCallbacks();
}, 3000);
handleAutomaticPreviewAuthentication(runtimeScene);
handleAutomaticGamesPlatformAuthentication(runtimeScene);
}
);
@@ -95,7 +131,7 @@ namespace gdjs {
connectionId ? `&connectionId=${connectionId}` : ''
}${
runtimeGame.isUsingGDevelopDevelopmentEnvironment() ? '&dev=true' : ''
}`;
}&allowLoginProviders=true`;
};
/**
@@ -104,7 +140,7 @@ namespace gdjs {
*/
const getPlayerAuthPlatform = (
runtimeScene: RuntimeScene
): 'electron' | 'cordova' | 'cordova-websocket' | 'web-iframe' | 'web' => {
): 'electron' | 'cordova-websocket' | 'web-iframe' | 'web' => {
const runtimeGame = runtimeScene.getGame();
const electron = runtimeGame.getRenderer().getElectron();
if (electron) {
@@ -119,13 +155,8 @@ namespace gdjs {
if (shouldAuthenticationUseIframe(runtimeScene)) return 'web-iframe';
if (typeof cordova !== 'undefined') {
if (cordova.platformId === 'ios') {
// The game is an iOS app.
return 'cordova-websocket';
}
// The game is an Android app.
return 'cordova';
// The game is an Android or an iOS app.
return 'cordova-websocket';
}
// This can be a:
@@ -304,7 +335,7 @@ namespace gdjs {
removeAuthenticationContainer(runtimeScene);
clearAuthenticationWindowTimeout();
// If there is a websocket communication (electron, cordova iOS), close it.
// If there is a websocket communication (electron, cordova), close it.
if (_websocket) {
logger.info('Closing authentication websocket connection.');
_websocket.close();
@@ -315,10 +346,17 @@ namespace gdjs {
_authenticationWindow.close();
_authenticationWindow = null;
}
// If an in-app browser was used (cordova), close it.
if (_authenticationInAppWindow) {
_authenticationInAppWindow.close();
_authenticationInAppWindow = null;
// If cordova (native mobile app), hide external window.
// TODO: calling hide does nothing on Android, the plugin should be updated to handle the action `hide`.
if (typeof SafariViewController !== 'undefined') {
try {
SafariViewController.hide();
} catch (error) {
logger.info(
'Could not hide login window. Waiting for user to do it.'
);
}
}
};
@@ -447,7 +485,7 @@ namespace gdjs {
username: event.data.body.username,
userToken: event.data.body.token,
});
removeAuthenticationCallbacks();
removeAutomaticGamesPlatformAuthenticationCallback();
refreshAuthenticationBannerIfAny(runtimeScene);
break;
}
@@ -505,8 +543,6 @@ namespace gdjs {
* - the authentication window is closed
*/
const clearAuthenticationWindowTimeout = () => {
if (_initialAuthenticationTimeoutId)
clearTimeout(_initialAuthenticationTimeoutId);
if (_authenticationTimeoutId) clearTimeout(_authenticationTimeoutId);
};
@@ -707,8 +743,8 @@ namespace gdjs {
);
/**
* Helper to handle authentication window on Cordova on iOS.
* We open an InAppBrowser window, and listen to the websocket to know when the user is logged in.
* Helper to handle authentication window on Cordova on iOS and Android.
* We open an external window, and listen to the websocket to know when the user is logged in.
*/
const openAuthenticationWindowForCordovaWithWebSocket = (
runtimeScene: gdjs.RuntimeScene,
@@ -724,80 +760,38 @@ namespace gdjs {
connectionId,
});
_authenticationInAppWindow = cordova.InAppBrowser.open(
targetUrl,
'authentication',
'location=yes,toolbarcolor=#000000,hidenavigationbuttons=yes,closebuttoncolor=#FFFFFF' // location=yes is important to show the URL bar to the user.
);
if (!_authenticationInAppWindow) {
resolve('errored');
return;
}
_authenticationInAppWindow.addEventListener(
'exit',
() => {
resolve('dismissed');
},
true
);
SafariViewController.isAvailable(function (available: boolean) {
if (available) {
SafariViewController.show(
{
url: targetUrl,
hidden: false,
animated: true,
transition: 'slide',
enterReaderModeIfAvailable: false,
barColor: '#000000',
tintColor: '#ffffff',
controlTintColor: '#ffffff',
},
function (result: any) {
// Other events are `opened` and `loaded`.
if (result.event === 'closed') {
resolve('dismissed');
}
},
function (error: any) {
logger.log('Error opening webview: ' + JSON.stringify(error));
resolve('errored');
}
);
} else {
logger.error('Plugin SafariViewController is not available');
resolve('errored');
}
});
}
);
/**
* Helper to handle authentication window on Cordova.
* We open an InAppBrowser window, and listen to messages posted on this window.
*/
const openAuthenticationWindowForCordova = (
runtimeScene: gdjs.RuntimeScene,
gameId: string
) =>
new Promise<AuthenticationWindowStatus>((resolve) => {
const targetUrl = getAuthWindowUrl({
runtimeGame: runtimeScene.getGame(),
gameId,
});
_authenticationInAppWindow = cordova.InAppBrowser.open(
targetUrl,
'authentication',
'location=yes,toolbarcolor=#000000,hidenavigationbuttons=yes,closebuttoncolor=#FFFFFF' // location=yes is important to show the URL bar to the user.
);
if (!_authenticationInAppWindow) {
resolve('errored');
return;
}
// Listen to messages posted on the authentication window, so that we can
// know when the user is authenticated.
let isDoneAlready = false;
_authenticationInAppWindow.addEventListener(
'message',
(event: MessageEvent) => {
receiveAuthenticationMessage({
runtimeScene,
event,
checkOrigin: false, // For Cordova we don't check the origin, as the message is read from the InAppBrowser directly.
onDone: (status) => {
if (isDoneAlready) return;
isDoneAlready = true;
resolve(status);
},
});
},
true
);
_authenticationInAppWindow.addEventListener(
'exit',
() => {
if (isDoneAlready) return;
isDoneAlready = true;
resolve('dismissed');
},
true
);
});
/**
* Helper to handle authentication window on web.
* We open a new window, and listen to messages posted back to the game window.
@@ -870,7 +864,7 @@ namespace gdjs {
!_authenticationLoaderContainer ||
!_authenticationTextContainer
) {
console.error(
logger.error(
"Can't open an authentication iframe - no iframe container, loader container or text container was opened for it."
);
return;
@@ -937,12 +931,12 @@ namespace gdjs {
return;
}
let isDimissedAlready = false;
let isDismissedAlready = false;
const onAuthenticationContainerDismissed = () => {
cleanUpAuthWindowAndTimeouts(runtimeScene);
displayAuthenticationBanner(runtimeScene);
isDimissedAlready = true;
isDismissedAlready = true;
resolve({ status: 'dismissed' });
};
@@ -1009,13 +1003,6 @@ namespace gdjs {
_gameId
);
break;
case 'cordova':
// The game is an Android app.
status = await openAuthenticationWindowForCordova(
runtimeScene,
_gameId
);
break;
case 'cordova-websocket':
// The game is an iOS app.
status = await openAuthenticationWindowForCordovaWithWebSocket(
@@ -1045,7 +1032,7 @@ namespace gdjs {
break;
}
if (isDimissedAlready) return;
if (isDismissedAlready) return;
if (status === 'dismissed') {
onAuthenticationContainerDismissed();
}
@@ -1106,6 +1093,24 @@ namespace gdjs {
}
};
/*
* Remove the automatic authentication callback when running on web.
*/
const removeAutomaticGamesPlatformAuthenticationCallback = function () {
if (_automaticGamesPlatformAuthenticationCallback) {
window.removeEventListener(
'message',
_automaticGamesPlatformAuthenticationCallback,
true
);
_automaticGamesPlatformAuthenticationCallback = null;
}
if (_automaticGamesPlatformAuthenticationTimeoutId) {
clearTimeout(_automaticGamesPlatformAuthenticationTimeoutId);
_automaticGamesPlatformAuthenticationTimeoutId = null;
}
};
/**
* Remove the banner displaying the authentication status.
*/

View File

@@ -0,0 +1,11 @@
Spine Runtimes License Agreement
Last updated February 20, 2024. Replaces all prior versions.
Copyright (c) 2013-2024, Esoteric Software LLC
Integration of the Spine Runtimes into software or otherwise creating derivative works of the Spine Runtimes is permitted under the terms and conditions of Section 2 of the Spine Editor License Agreement:
http://esotericsoftware.com/spine-editor-license
Otherwise, it is permitted to integrate the Spine Runtimes into software or otherwise create derivative works of the Spine Runtimes (collectively, "Products"), provided that each user of the Products must obtain their own Spine Editor license and redistribution of the Products in any form must include this license and copyright notice.
THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -12,6 +12,24 @@ namespace gdjs {
};
export type SpineObjectData = ObjectData & SpineObjectDataType;
export type SpineNetworkSyncDataType = {
opa: float;
wid: float;
hei: float;
scaX: float;
scaY: float;
flipX: boolean;
flipY: boolean;
ani: number;
anmd: number;
anp: boolean;
anss: float;
anet: number;
};
export type SpineNetworkSyncData = ObjectNetworkSyncData &
SpineNetworkSyncDataType;
export class SpineRuntimeObject
extends gdjs.RuntimeObject
implements
@@ -94,6 +112,86 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SpineNetworkSyncData {
return {
...super.getNetworkSyncData(),
opa: this._opacity,
wid: this.getWidth(),
hei: this.getHeight(),
scaX: this.getScaleX(),
scaY: this.getScaleY(),
flipX: this.isFlippedX(),
flipY: this.isFlippedY(),
ani: this.getAnimationIndex(),
anmd: this.getAnimationMixingDuration(),
anp: this.isAnimationPaused(),
anss: this.getAnimationSpeedScale(),
anet: this.getAnimationElapsedTime(),
};
}
updateFromNetworkSyncData(syncData: SpineNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
if (syncData.opa !== undefined && syncData.opa !== this._opacity) {
this.setOpacity(syncData.opa);
}
if (syncData.wid !== undefined && syncData.wid !== this.getWidth()) {
this.setWidth(syncData.wid);
}
if (syncData.hei !== undefined && syncData.hei !== this.getHeight()) {
this.setHeight(syncData.hei);
}
if (syncData.scaX !== undefined && syncData.scaX !== this.getScaleX()) {
this.setScaleX(syncData.scaX);
}
if (syncData.scaY !== undefined && syncData.scaY !== this.getScaleY()) {
this.setScaleY(syncData.scaY);
}
if (
syncData.flipX !== undefined &&
syncData.flipX !== this.isFlippedX()
) {
this.flipX(syncData.flipX);
}
if (
syncData.flipY !== undefined &&
syncData.flipY !== this.isFlippedY()
) {
this.flipY(syncData.flipY);
}
if (
syncData.ani !== undefined &&
syncData.ani !== this.getAnimationIndex()
) {
this.setAnimationIndex(syncData.ani);
}
if (
syncData.anmd !== undefined &&
syncData.anmd !== this.getAnimationMixingDuration()
) {
this.setAnimationMixingDuration(syncData.anmd);
}
if (
syncData.anp !== undefined &&
syncData.anp !== this.isAnimationPaused()
) {
syncData.anp ? this.pauseAnimation() : this.resumeAnimation();
}
if (
syncData.anss !== undefined &&
syncData.anss !== this.getAnimationSpeedScale()
) {
this.setAnimationSpeedScale(syncData.anss);
}
if (
syncData.anet !== undefined &&
syncData.anet !== this.getAnimationElapsedTime()
) {
this.setAnimationElapsedTime(syncData.anet);
}
}
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {

View File

@@ -42,6 +42,28 @@ namespace gdjs {
};
}
export type TextInputNetworkSyncDataType = {
opa: float;
wid: float;
hei: float;
txt: string;
frn: string;
fs: number;
place: string;
it: SupportedInputType;
tc: string;
fc: string;
fo: float;
bc: string;
bo: float;
bw: float;
dis: boolean;
ro: boolean;
};
export type TextInputNetworkSyncData = ObjectNetworkSyncData &
TextInputNetworkSyncDataType;
const DEFAULT_WIDTH = 300;
const DEFAULT_HEIGHT = 30;
@@ -170,6 +192,49 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TextInputNetworkSyncData {
return {
...super.getNetworkSyncData(),
opa: this.getOpacity(),
wid: this.getWidth(),
hei: this.getHeight(),
txt: this.getText(),
frn: this.getFontResourceName(),
fs: this.getFontSize(),
place: this.getPlaceholder(),
it: this.getInputType(),
tc: this.getTextColor(),
fc: this.getFillColor(),
fo: this.getFillOpacity(),
bc: this.getBorderColor(),
bo: this.getBorderOpacity(),
bw: this.getBorderWidth(),
dis: this.isDisabled(),
ro: this.isReadOnly(),
};
}
updateFromNetworkSyncData(syncData: TextInputNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
if (syncData.opa !== undefined) this.setOpacity(syncData.opa);
if (syncData.wid !== undefined) this.setWidth(syncData.wid);
if (syncData.hei !== undefined) this.setHeight(syncData.hei);
if (syncData.txt !== undefined) this.setText(syncData.txt);
if (syncData.frn !== undefined) this.setFontResourceName(syncData.frn);
if (syncData.fs !== undefined) this.setFontSize(syncData.fs);
if (syncData.place !== undefined) this.setPlaceholder(syncData.place);
if (syncData.it !== undefined) this.setInputType(syncData.it);
if (syncData.tc !== undefined) this.setTextColor(syncData.tc);
if (syncData.fc !== undefined) this.setFillColor(syncData.fc);
if (syncData.fo !== undefined) this.setFillOpacity(syncData.fo);
if (syncData.bc !== undefined) this.setBorderColor(syncData.bc);
if (syncData.bo !== undefined) this.setBorderOpacity(syncData.bo);
if (syncData.bw !== undefined) this.setBorderWidth(syncData.bw);
if (syncData.dis !== undefined) this.setDisabled(syncData.dis);
if (syncData.ro !== undefined) this.setReadOnly(syncData.ro);
}
updatePreRender(instanceContainer: RuntimeInstanceContainer): void {
this._renderer.updatePreRender();
}

View File

@@ -205,9 +205,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): TextObjectNetworkSyncData {
getNetworkSyncData(): TextObjectNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
str: this._str,
o: this.opacity,
cs: this._characterSize,
@@ -233,10 +233,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: TextObjectNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.str !== undefined) {
this.setText(networkSyncData.str);
}

View File

@@ -181,9 +181,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): TilemapCollisionMaskNetworkSyncData {
getNetworkSyncData(): TilemapCollisionMaskNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
tmjf: this.getTilemapJsonFile(),
tsjf: this.getTilesetJsonFile(),
dm: this.getDebugMode(),
@@ -197,10 +197,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: TilemapCollisionMaskNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.tmjf !== undefined) {
this.setTilemapJsonFile(networkSyncData.tmjf);

View File

@@ -147,9 +147,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): TilemapNetworkSyncData {
getNetworkSyncData(): TilemapNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
op: this._opacity,
tmjf: this._tilemapJsonFile,
tsjf: this._tilesetJsonFile,
@@ -163,10 +163,8 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
networkSyncData: TilemapNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(networkSyncData: TilemapNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);

View File

@@ -79,9 +79,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): TiledSpriteNetworkSyncData {
getNetworkSyncData(): TiledSpriteNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
wid: this.getWidth(),
hei: this.getHeight(),
xo: this.getXOffset(),
@@ -91,10 +91,10 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
updateFromNetworkSyncData(
networkSyncData: TiledSpriteNetworkSyncData
): void {
super.updateFromObjectNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -4,6 +4,24 @@ Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
interface TopDownMovementNetworkSyncDataType {
a: float;
xv: float;
yv: float;
as: float;
lk: boolean;
rk: boolean;
uk: boolean;
dk: boolean;
wsu: boolean;
sa: float;
sf: float;
}
export interface TopDownMovementNetworkSyncData
extends BehaviorNetworkSyncData {
props: TopDownMovementNetworkSyncDataType;
}
/**
* Allows an object to move in 4 or 8 directions, with customizable speed, accelerations
* and rotation.
@@ -45,6 +63,10 @@ namespace gdjs {
// 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;
// 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.
_ignoreDefaultControlsAsSyncedByNetwork: boolean = false;
// This is useful for extensions that need to know
// which keys were pressed and doesn't know the mapping
@@ -83,10 +105,11 @@ namespace gdjs {
this._movementAngleOffset = behaviorData.movementAngleOffset || 0;
}
getNetworkSyncData() {
getNetworkSyncData(): 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._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
@@ -106,7 +129,9 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: BehaviorNetworkSyncData): void {
updateFromNetworkSyncData(
networkSyncData: TopDownMovementNetworkSyncData
): void {
super.updateFromNetworkSyncData(networkSyncData);
const behaviorSpecificProps = networkSyncData.props;
@@ -146,6 +171,8 @@ namespace gdjs {
// 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;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -324,19 +351,19 @@ namespace gdjs {
//Get the player input:
this._leftKey ||
(this._leftKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(LEFTKEY));
this._rightKey ||
(this._rightKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(RIGHTKEY));
this._downKey ||
(this._downKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(DOWNKEY));
this._upKey ||
(this._upKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(UPKEY));
const elapsedTime = this.owner.getElapsedTime();
@@ -579,6 +606,13 @@ namespace gdjs {
this._ignoreDefaultControls = ignore;
}
shouldIgnoreDefaultControls() {
return (
this._ignoreDefaultControls ||
this._ignoreDefaultControlsAsSyncedByNetwork
);
}
simulateLeftKey() {
this._leftKey = true;
}

View File

@@ -16,6 +16,20 @@ namespace gdjs {
export type VideoObjectData = ObjectData & VideoObjectDataType;
export type VideoNetworkSyncDataType = {
op: float;
wid: float;
hei: float;
// We don't sync volume, as it's probably a user setting?
pla: boolean;
loop: boolean;
ct: float;
ps: number;
};
export type VideoNetworkSyncData = ObjectNetworkSyncData &
VideoNetworkSyncDataType;
/**
* An object displaying a video on screen.
*
@@ -86,6 +100,52 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): VideoNetworkSyncData {
return {
...super.getNetworkSyncData(),
op: this._opacity,
wid: this.getWidth(),
hei: this.getHeight(),
pla: this.isPlayed(),
loop: this.isLooped(),
ct: this.getCurrentTime(),
ps: this.getPlaybackSpeed(),
};
}
updateFromNetworkSyncData(syncData: VideoNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
if (this._opacity !== undefined && this._opacity && syncData.op) {
this.setOpacity(syncData.op);
}
if (this.getWidth() !== undefined && this.getWidth() !== syncData.wid) {
this.setWidth(syncData.wid);
}
if (this.getHeight() !== undefined && this.getHeight() !== syncData.hei) {
this.setHeight(syncData.hei);
}
if (syncData.pla !== undefined && this.isPlayed() !== syncData.pla) {
syncData.pla ? this.play() : this.pause();
}
if (syncData.loop !== undefined && this.isLooped() !== syncData.loop) {
this.setLoop(syncData.loop);
}
// We don't update the current time too regularly, only if it's off by a lot.
if (
syncData.ct !== undefined &&
Math.abs(this.getCurrentTime() - syncData.ct) > 3 // More than 3 seconds off
) {
this.setCurrentTime(syncData.ct);
}
if (
syncData.ps !== undefined &&
this.getPlaybackSpeed() !== syncData.ps
) {
this.setPlaybackSpeed(syncData.ps);
}
}
/**
* Initialize the extra parameters that could be set for an instance.
* @param initialInstanceData The initial instance data

View File

@@ -6,8 +6,8 @@
#include "BehaviorCodeGenerator.h"
#include "EventsCodeGenerator.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/EventsBasedBehavior.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
namespace gdjs {
@@ -223,6 +223,7 @@ CODE_NAMESPACE.RUNTIME_BEHAVIOR_CLASSNAME = class RUNTIME_BEHAVIOR_CLASSNAME ext
};
}
updateFromNetworkSyncData(networkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
UPDATE_FROM_NETWORK_SYNC_DATA_CODE
}

View File

@@ -445,6 +445,57 @@ MetadataDeclarationHelper::DeclareInstructionOrExpressionMetadata(
}
}
const gd::String &MetadataDeclarationHelper::GetFullName(const gd::EventsFunction &eventsFunction) {
return eventsFunction.GetFullName() || eventsFunction.GetName();
};
gd::String MetadataDeclarationHelper::GetDefaultSentence(
const gd::EventsFunction &eventsFunction, const int firstParameterIndex,
const int parameterIndexOffset) {
gd::String defaultSentence = GetFullName(eventsFunction);
auto &parameters = eventsFunction.GetParameters();
if (parameters.size() == 0) {
return defaultSentence;
}
defaultSentence += " (";
for (size_t parameterIndex = firstParameterIndex;
parameterIndex < parameters.size(); parameterIndex++) {
auto &parameter = parameters.at(parameterIndex);
defaultSentence += parameter.GetName() + ": _PARAM" +
gd::String::From(parameterIndex + parameterIndexOffset) +
"_";
if (parameterIndex < parameters.size() - 1) {
defaultSentence += ", ";
}
}
defaultSentence += ")";
return defaultSentence;
};
gd::String MetadataDeclarationHelper::GetFreeFunctionSentence(const gd::EventsFunction &eventsFunction) {
return eventsFunction.GetSentence().empty()
? GetDefaultSentence(eventsFunction, 0, 1)
: eventsFunction.GetSentence();
};
gd::String MetadataDeclarationHelper::GetBehaviorFunctionSentence(
const gd::EventsFunction &eventsFunction,
const bool excludeObjectParameter) {
return eventsFunction.GetSentence().empty()
? GetDefaultSentence(eventsFunction,
excludeObjectParameter ? 2 : 0, 0)
: eventsFunction.GetSentence();
};
gd::String MetadataDeclarationHelper::GetObjectFunctionSentence(
const gd::EventsFunction &eventsFunction,
const bool excludeObjectParameter) {
return eventsFunction.GetSentence().empty()
? GetDefaultSentence(eventsFunction,
excludeObjectParameter ? 1 : 0, 0)
: eventsFunction.GetSentence();
};
/**
* Declare the instruction (action/condition) or expression for the given
* (free) events function.
@@ -460,11 +511,11 @@ MetadataDeclarationHelper::DeclareExpressionMetadata(
gd::ValueTypeMetadata::GetPrimitiveValueType(
eventsFunction.GetExpressionType().GetName()),
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
RemoveTrailingDot(eventsFunction.GetDescription()) ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(eventsFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetFreeFunctionSentence(eventsFunction), 2),
eventsFunction.GetGroup(), GetExtensionIconUrl(extension));
// By convention, first parameter is always the Runtime Scene.
expressionAndCondition.AddCodeOnlyParameter("currentScene", "");
@@ -477,15 +528,15 @@ MetadataDeclarationHelper::DeclareExpressionMetadata(
eventsFunction.GetExpressionType().IsNumber()
? extension.AddExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup(), GetExtensionIconUrl(extension))
: extension.AddStrExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup(), GetExtensionIconUrl(extension));
// By convention, first parameter is always the Runtime Scene.
expression.AddCodeOnlyParameter("currentScene", "");
@@ -505,17 +556,17 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
const gd::EventsFunction &eventsFunction) {
auto functionType = eventsFunction.GetFunctionType();
if (functionType == gd::EventsFunction::Condition) {
auto &action = extension.AddCondition(
auto &condition = extension.AddCondition(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(), eventsFunction.GetGroup(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetFreeFunctionSentence(eventsFunction), eventsFunction.GetGroup(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
// By convention, first parameter is always the Runtime Scene.
action.AddCodeOnlyParameter("currentScene", "");
condition.AddCodeOnlyParameter("currentScene", "");
DeclareEventsFunctionParameters(eventsFunctionsExtension, eventsFunction,
action, 0);
return action;
condition, 0);
return condition;
} else if (functionType == gd::EventsFunction::ActionWithOperator) {
if (eventsFunctionsExtension.HasEventsFunctionNamed(
eventsFunction.GetGetterName())) {
@@ -524,11 +575,11 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
auto &action = extension.AddAction(
eventsFunction.GetName(),
getterFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(getterFunction),
"Change " +
(getterFunction.GetDescription() || eventsFunction.GetFullName()),
(getterFunction.GetDescription() || GetFullName(getterFunction)),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(getterFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetFreeFunctionSentence(getterFunction), 2),
getterFunction.GetGroup(), GetExtensionIconUrl(extension),
GetExtensionIconUrl(extension));
action
@@ -547,7 +598,7 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
auto &action = extension.AddAction(
eventsFunction.GetName(), eventsFunction.GetName(),
_("Change <subject>")
.FindAndReplace("<subject>", eventsFunction.GetFullName()),
.FindAndReplace("<subject>", GetFullName(eventsFunction)),
// An operator and an operand are inserted before user parameters.
"", "", GetExtensionIconUrl(extension),
GetExtensionIconUrl(extension));
@@ -560,9 +611,9 @@ gd::InstructionMetadata &MetadataDeclarationHelper::DeclareInstructionMetadata(
} else {
auto &action = extension.AddAction(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(), eventsFunction.GetGroup(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetFreeFunctionSentence(eventsFunction), eventsFunction.GetGroup(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
// By convention, first parameter is always the Runtime Scene.
action.AddCodeOnlyParameter("currentScene", "");
@@ -650,11 +701,11 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata(
gd::ValueTypeMetadata::GetPrimitiveValueType(
eventsFunction.GetExpressionType().GetName()),
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
RemoveTrailingDot(eventsFunction.GetDescription()) ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(eventsFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetBehaviorFunctionSentence(eventsFunction, true), 2),
eventsFunction.GetGroup() || eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
GetExtensionIconUrl(extension));
@@ -667,18 +718,18 @@ MetadataDeclarationHelper::DeclareBehaviorExpressionMetadata(
(eventsFunction.GetExpressionType().IsNumber())
? behaviorMetadata.AddExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup() ||
eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
GetExtensionIconUrl(extension))
: behaviorMetadata.AddStrExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup() ||
eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
@@ -705,9 +756,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
// behaviors (that can totally have functions with the same name).
auto &condition = behaviorMetadata.AddScopedCondition(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetBehaviorFunctionSentence(eventsFunction),
eventsFunction.GetGroup() || eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -722,12 +773,12 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
eventsFunction.GetGetterName());
auto &action = behaviorMetadata.AddScopedAction(
eventsFunction.GetName(),
getterFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(getterFunction),
_("Change <subject>")
.FindAndReplace("<subject>", getterFunction.GetDescription() ||
eventsFunction.GetFullName()),
GetFullName(getterFunction)),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(getterFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetBehaviorFunctionSentence(getterFunction, true), 2),
getterFunction.GetGroup() || eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -743,7 +794,7 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
auto &action = behaviorMetadata.AddScopedAction(
eventsFunction.GetName(), eventsFunction.GetName(),
_("Change <subject>")
.FindAndReplace("<subject>", eventsFunction.GetFullName()),
.FindAndReplace("<subject>", GetFullName(eventsFunction)),
// An operator and an operand are inserted before user parameters.
"",
eventsBasedBehavior.GetFullName() || eventsBasedBehavior.GetName(),
@@ -759,9 +810,9 @@ MetadataDeclarationHelper::DeclareBehaviorInstructionMetadata(
// behaviors (that can totally have functions with the same name).
auto &action = behaviorMetadata.AddScopedAction(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetBehaviorFunctionSentence(eventsFunction),
eventsFunction.GetGroup() || eventsBasedBehavior.GetFullName() ||
eventsBasedBehavior.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -823,11 +874,11 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata(
gd::ValueTypeMetadata::GetPrimitiveValueType(
eventsFunction.GetExpressionType().GetName()),
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
RemoveTrailingDot(eventsFunction.GetDescription()) ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(eventsFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetObjectFunctionSentence(eventsFunction, true), 2),
eventsFunction.GetGroup() || eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
GetExtensionIconUrl(extension));
@@ -841,18 +892,18 @@ MetadataDeclarationHelper::DeclareObjectExpressionMetadata(
(eventsFunction.GetExpressionType().IsNumber())
? objectMetadata.AddExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup() ||
eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
GetExtensionIconUrl(extension))
: objectMetadata.AddStrExpression(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() ||
eventsFunction.GetFullName(),
GetFullName(eventsFunction),
eventsFunction.GetGroup() ||
eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
@@ -880,9 +931,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
// objects (that can totally have functions with the same name).
auto &condition = objectMetadata.AddScopedCondition(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetObjectFunctionSentence(eventsFunction),
eventsFunction.GetGroup() || eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -898,11 +949,11 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
eventsFunction.GetGetterName());
auto &action = objectMetadata.AddScopedAction(
eventsFunction.GetName(),
getterFunction.GetFullName() || eventsFunction.GetName(),
GetFullName(getterFunction),
"Change " +
(getterFunction.GetDescription() || eventsFunction.GetFullName()),
(getterFunction.GetDescription() || GetFullName(getterFunction)),
// An operator and an operand are inserted before user parameters.
ShiftSentenceParamIndexes(getterFunction.GetSentence(), 2),
ShiftSentenceParamIndexes(GetObjectFunctionSentence(getterFunction, true), 2),
getterFunction.GetGroup() || eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -918,7 +969,7 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
auto &action = objectMetadata.AddScopedAction(
eventsFunction.GetName(), eventsFunction.GetName(),
_("Change <subject>")
.FindAndReplace("<subject>", eventsFunction.GetFullName()),
.FindAndReplace("<subject>", GetFullName(eventsFunction)),
// An operator and an operand are inserted before user parameters.
"", eventsBasedObject.GetFullName() || eventsBasedObject.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -933,9 +984,9 @@ MetadataDeclarationHelper::DeclareObjectInstructionMetadata(
// objects (that can totally have functions with the same name).
auto &action = objectMetadata.AddScopedAction(
eventsFunction.GetName(),
eventsFunction.GetFullName() || eventsFunction.GetName(),
eventsFunction.GetDescription() || eventsFunction.GetFullName(),
eventsFunction.GetSentence(),
GetFullName(eventsFunction),
eventsFunction.GetDescription() || GetFullName(eventsFunction),
GetObjectFunctionSentence(eventsFunction),
eventsFunction.GetGroup() || eventsBasedObject.GetFullName() ||
eventsBasedObject.GetName(),
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
@@ -983,15 +1034,13 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
auto group = (eventsBasedEntity.GetFullName() || eventsBasedEntity.GetName())
+ " " + property.GetGroup() + " properties";
auto uncapitalizedLabel =
UncapitalizeFirstLetter(property.GetLabel()) || property.GetName();
if (propertyType == "Boolean") {
auto &conditionMetadata = entityMetadata.AddScopedCondition(
conditionName, propertyLabel,
_("Check the property value for <property_name>.")
.FindAndReplace("<property_name>", uncapitalizedLabel),
.FindAndReplace("<property_name>", property.GetName()),
_("Property <property_name> of _PARAM0_ is true")
.FindAndReplace("<property_name>", uncapitalizedLabel),
.FindAndReplace("<property_name>", property.GetName()),
group,
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(conditionMetadata);
@@ -999,11 +1048,11 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
auto &setterActionMetadata = entityMetadata.AddScopedAction(
actionName, propertyLabel,
_("Update the property value for \"<property_name>\".")
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("Update the property value for <property_name>.")
.FindAndReplace("<property_name>", property.GetName()),
_("Set property value for <property_name> of _PARAM0_ to "
"<property_value>")
.FindAndReplace("<property_name>", uncapitalizedLabel)
.FindAndReplace("<property_name>", property.GetName())
.FindAndReplace("<property_value>",
"_PARAM" + gd::String::From(valueParameterIndex) +
"_"),
@@ -1019,9 +1068,9 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
_("Toggle the property value for <property_name>.\n"
"If it was true, it will become false, and if it was false it "
"will become true.")
.FindAndReplace("<property_name>", uncapitalizedLabel),
.FindAndReplace("<property_name>", property.GetName()),
_("Toggle property <property_name> of _PARAM0_")
.FindAndReplace("<property_name>", uncapitalizedLabel),
.FindAndReplace("<property_name>", property.GetName()),
group,
GetExtensionIconUrl(extension), GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(toggleActionMetadata);
@@ -1036,10 +1085,10 @@ void MetadataDeclarationHelper::DeclarePropertyInstructionAndExpression(
gd::ValueTypeMetadata::GetPrimitiveValueType(
gd::ValueTypeMetadata::ConvertPropertyTypeToValueType(propertyType)),
expressionName, propertyLabel,
_("the property value for the <property_name>")
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("the property value for the <property_name>")
.FindAndReplace("<property_name>", uncapitalizedLabel),
_("the property value for <property_name>")
.FindAndReplace("<property_name>", property.GetName()),
_("the property value for <property_name>")
.FindAndReplace("<property_name>", property.GetName()),
group,
GetExtensionIconUrl(extension));
addObjectAndBehaviorParameters(propertyInstructionMetadata);
@@ -1269,7 +1318,8 @@ void MetadataDeclarationHelper::AddParameter(
const gd::ParameterMetadata &parameter) {
if (!parameter.IsCodeOnly()) {
instructionOrExpression
.AddParameter(parameter.GetType(), parameter.GetDescription(),
.AddParameter(parameter.GetType(),
parameter.GetDescription() || parameter.GetName(),
"", // See below for adding the extra information
parameter.IsOptional())
// Manually add the "extra info" without relying on addParameter (or

View File

@@ -324,6 +324,20 @@ private:
static gd::String UncapitalizeFirstLetter(const gd::String &string);
static const gd::String &
GetFullName(const gd::EventsFunction &eventsFunction);
static gd::String GetDefaultSentence(const gd::EventsFunction &eventsFunction,
const int firstParameterIndex,
const int parameterIndexOffset);
static gd::String
GetFreeFunctionSentence(const gd::EventsFunction &eventsFunction);
static gd::String
GetBehaviorFunctionSentence(const gd::EventsFunction &eventsFunction,
const bool excludeObjectParameter = false);
static gd::String
GetObjectFunctionSentence(const gd::EventsFunction &eventsFunction,
const bool excludeObjectParameter = false);
std::vector<gd::MultipleInstructionMetadata> expressionAndConditions;
};

View File

@@ -17,8 +17,8 @@
#include <string>
#include "GDCore/CommonTools.h"
#include "GDCore/Events/CodeGeneration/EffectsCodeGenerator.h"
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
#include "GDCore/Events/CodeGeneration/EffectsCodeGenerator.h"
#include "GDCore/Extensions/Metadata/DependencyMetadata.h"
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
#include "GDCore/Extensions/Platform.h"
@@ -178,8 +178,11 @@ bool ExporterHelper::ExportProjectForPixiPreview(
wholeProjectDiagnosticReport.Clear();
// Generate events code
if (!ExportEventsCode(immutableProject, codeOutputDir, includesFiles,
wholeProjectDiagnosticReport, true)) {
if (!ExportEventsCode(immutableProject,
codeOutputDir,
includesFiles,
wholeProjectDiagnosticReport,
true)) {
return false;
}
@@ -199,11 +202,11 @@ bool ExporterHelper::ExportProjectForPixiPreview(
gd::SceneResourcesFinder::FindProjectResources(exportedProject);
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < exportedProject.GetLayoutsCount(); layoutIndex++) {
layoutIndex < exportedProject.GetLayoutsCount();
layoutIndex++) {
auto &layout = exportedProject.GetLayout(layoutIndex);
scenesUsedResources[layout.GetName()] =
gd::SceneResourcesFinder::FindSceneResources(exportedProject,
layout);
gd::SceneResourcesFinder::FindSceneResources(exportedProject, layout);
}
// Strip the project (*after* generating events as the events may use stripped
@@ -222,6 +225,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("nativeMobileApp")
.SetBoolValue(options.nativeMobileApp);
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
.SetStringValue(options.websocketDebuggerServerAddress);
runtimeGameOptions.AddChild("websocketDebuggerServerPort")
@@ -237,6 +242,13 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
runtimeGameOptions.AddChild("allowAuthenticationUsingIframeForPreview")
.SetBoolValue(options.allowAuthenticationUsingIframeForPreview);
if (!options.playerId.empty() && !options.playerToken.empty()) {
runtimeGameOptions.AddChild("playerUsername")
.SetStringValue(options.playerUsername);
runtimeGameOptions.AddChild("playerId").SetStringValue(options.playerId);
runtimeGameOptions.AddChild("playerToken")
.SetStringValue(options.playerToken);
}
// Pass in the options the list of scripts files - useful for hot-reloading.
auto &scriptFilesElement = runtimeGameOptions.AddChild("scriptFiles");
@@ -253,8 +265,11 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
// Export the project
ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
runtimeGameOptions, projectUsedResources,
ExportProjectData(fs,
exportedProject,
codeOutputDir + "/data.js",
runtimeGameOptions,
projectUsedResources,
scenesUsedResources);
includesFiles.push_back(codeOutputDir + "/data.js");
@@ -289,7 +304,8 @@ gd::String ExporterHelper::ExportProjectData(
// Save the project to JSON
gd::SerializerElement rootElement;
project.SerializeTo(rootElement);
SerializeUsedResources(rootElement, projectUsedResources, scenesUsedResources);
SerializeUsedResources(
rootElement, projectUsedResources, scenesUsedResources);
gd::String output =
"gdjs.projectData = " + gd::Serializer::ToJSON(rootElement) + ";\n" +
"gdjs.runtimeGameOptions = " +
@@ -304,7 +320,6 @@ void ExporterHelper::SerializeUsedResources(
gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
auto serializeUsedResources =
[](gd::SerializerElement &element,
std::set<gd::String> &usedResources) -> void {
@@ -320,7 +335,8 @@ void ExporterHelper::SerializeUsedResources(
auto &layoutsElement = rootElement.GetChild("layouts");
for (std::size_t layoutIndex = 0;
layoutIndex < layoutsElement.GetChildrenCount(); layoutIndex++) {
layoutIndex < layoutsElement.GetChildrenCount();
layoutIndex++) {
auto &layoutElement = layoutsElement.GetChild(layoutIndex);
const auto layoutName = layoutElement.GetStringAttribute("name");
@@ -555,7 +571,7 @@ bool ExporterHelper::ExportFacebookInstantGamesFiles(const gd::Project &project,
}
bool ExporterHelper::ExportHtml5Files(const gd::Project &project,
gd::String exportDir) {
gd::String exportDir) {
if (!fs.WriteToFile(exportDir + "/manifest.webmanifest",
GenerateWebManifest(project))) {
lastError = "Unable to export WebManifest.";
@@ -799,7 +815,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "pixi-renderers/three.js");
InsertUnique(includesFiles, "pixi-renderers/ThreeAddons.js");
InsertUnique(includesFiles, "pixi-renderers/draco/gltf/draco_decoder.wasm");
InsertUnique(includesFiles, "pixi-renderers/draco/gltf/draco_wasm_wrapper.js");
InsertUnique(includesFiles,
"pixi-renderers/draco/gltf/draco_wasm_wrapper.js");
}
if (pixiRenderers) {
InsertUnique(includesFiles, "pixi-renderers/pixi.js");
@@ -811,7 +828,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "pixi-renderers/pixi-bitmapfont-manager.js");
InsertUnique(includesFiles,
"pixi-renderers/spriteruntimeobject-pixi-renderer.js");
InsertUnique(includesFiles, "pixi-renderers/CustomRuntimeObject2DPixiRenderer.js");
InsertUnique(includesFiles,
"pixi-renderers/CustomRuntimeObject2DPixiRenderer.js");
InsertUnique(includesFiles, "pixi-renderers/DebuggerPixiRenderer.js");
InsertUnique(includesFiles,
"pixi-renderers/loadingscreen-pixi-renderer.js");
@@ -828,7 +846,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3D.js");
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3DRenderer.js");
InsertUnique(includesFiles, "Extensions/3D/CustomRuntimeObject3D.js");
InsertUnique(includesFiles, "Extensions/3D/CustomRuntimeObject3DRenderer.js");
InsertUnique(includesFiles,
"Extensions/3D/CustomRuntimeObject3DRenderer.js");
}
}
@@ -859,7 +878,8 @@ bool ExporterHelper::ExportEffectIncludes(
}
bool ExporterHelper::ExportEventsCode(
const gd::Project &project, gd::String outputDir,
const gd::Project &project,
gd::String outputDir,
std::vector<gd::String> &includesFiles,
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport,
bool exportForPreview) {
@@ -869,13 +889,12 @@ bool ExporterHelper::ExportEventsCode(
std::set<gd::String> eventsIncludes;
const gd::Layout &layout = project.GetLayout(i);
auto &diagnosticReport = wholeProjectDiagnosticReport.AddNewDiagnosticReportForScene(
auto &diagnosticReport =
wholeProjectDiagnosticReport.AddNewDiagnosticReportForScene(
layout.GetName());
LayoutCodeGenerator layoutCodeGenerator(project);
gd::String eventsOutput = layoutCodeGenerator.GenerateLayoutCompleteCode(
layout, eventsIncludes,
diagnosticReport,
!exportForPreview);
layout, eventsIncludes, diagnosticReport, !exportForPreview);
gd::String filename =
outputDir + "/" + "code" + gd::String::From(i) + ".js";

View File

@@ -8,8 +8,8 @@
#include <map>
#include <set>
#include <string>
#include <vector>
#include <unordered_map>
#include <vector>
#include "GDCore/String.h"
namespace gd {
@@ -37,12 +37,16 @@ struct PreviewExportOptions {
: project(project_),
exportPath(exportPath_),
useWindowMessageDebuggerClient(false),
nativeMobileApp(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
nonRuntimeScriptsCacheBurst(0),
fallbackAuthorId(""),
fallbackAuthorUsername(""),
playerId(""),
playerUsername(""),
playerToken(""),
allowAuthenticationUsingIframeForPreview(false){};
/**
@@ -67,6 +71,19 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set the fallback author info (if info not present in project
* properties).
*/
PreviewExportOptions &SetAuthenticatedPlayer(const gd::String &id,
const gd::String &username,
const gd::String &token) {
playerId = id;
playerUsername = username;
playerToken = token;
return *this;
}
/**
* \brief Set that the game should connect to the debugger server using
* the "Window Message " debugger client.
@@ -76,6 +93,15 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set that the preview is launched from a GDevelop native mobile app
* (iOS or Android).
*/
PreviewExportOptions &SetNativeMobileApp(bool enable) {
nativeMobileApp = enable;
return *this;
}
/**
* \brief Set the layout to be run first in the previewed game
*/
@@ -186,6 +212,10 @@ struct PreviewExportOptions {
gd::String externalLayoutName;
gd::String fallbackAuthorUsername;
gd::String fallbackAuthorId;
gd::String playerId;
gd::String playerUsername;
gd::String playerToken;
bool nativeMobileApp;
std::map<gd::String, int> includeFileHashes;
bool projectDataOnlyExport;
bool fullLoadingScreen;
@@ -266,12 +296,14 @@ class ExporterHelper {
* in gdjs.runtimeGameOptions \return Empty string if everything is ok,
* description of the error otherwise.
*/
static gd::String
ExportProjectData(gd::AbstractFileSystem &fs, gd::Project &project,
gd::String filename,
const gd::SerializerElement &runtimeGameOptions,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &layersUsedResources);
static gd::String ExportProjectData(
gd::AbstractFileSystem &fs,
gd::Project &project,
gd::String filename,
const gd::SerializerElement &runtimeGameOptions,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources);
/**
* \brief Copy all the resources of the project to to the export directory,
@@ -327,7 +359,8 @@ class ExporterHelper {
* be exported along with the project. ( including "codeX.js" files ).
*/
bool ExportEventsCode(
const gd::Project &project, gd::String outputDir,
const gd::Project &project,
gd::String outputDir,
std::vector<gd::String> &includesFiles,
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport,
bool exportForPreview);
@@ -487,11 +520,12 @@ class ExporterHelper {
gd::String codeOutputDir; ///< The directory where JS code is outputted. Will
///< be then copied to the final output directory.
private:
private:
static void SerializeUsedResources(
gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &layersUsedResources);
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources);
};
} // namespace gdjs

View File

@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="GDJS_PACKAGENAME" version="GDJS_PROJECTVERSION" xmlns="http://www.w3.org/ns/widgets"
xmlns:cdv="http://cordova.apache.org/ns/1.0">
xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>GDJS_PROJECTNAME</name>
<content src="index.html" />
<plugin name="cordova-plugin-whitelist" version="1" />
@@ -24,6 +24,15 @@
<!-- GDJS_ICONS_ANDROID -->
<preference name="AndroidWindowSplashScreenBackground" value="#000000" />
<!-- Required to get cordova-plugin-safariviewcontroller to call Chrome CustomTabs on Android. -->
<config-file target="AndroidManifest.xml" parent="/manifest">
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</config-file>
</platform>
<platform name="ios">
<allow-intent href="itms:*" />

View File

@@ -397,9 +397,7 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(
networkSyncData: SpriteAnimatorNetworkSyncData
) {
updateFromNetworkSyncData(networkSyncData: SpriteAnimatorNetworkSyncData) {
this._currentAnimation = networkSyncData.an;
this._currentDirection = networkSyncData.di;
this._currentFrameIndex = networkSyncData.fr;

View File

@@ -40,6 +40,10 @@ namespace gdjs {
_nameId: integer;
_activated: boolean = true;
// When synchronised over the network, a behavior is always owned by the player owning the object,
// and always synced. If set to false, the behavior properties will not be synced to others.
_syncOverNetwork: boolean = true;
/**
* @param instanceContainer The container owning the object of the behavior
* @param behaviorData The properties used to setup the behavior
@@ -230,6 +234,14 @@ namespace gdjs {
usesLifecycleFunction(): boolean {
return true;
}
enableSynchronization(enable: boolean) {
this._syncOverNetwork = enable;
}
isSyncedOverNetwork(): boolean {
return this._syncOverNetwork;
}
}
gdjs.registerBehavior('', gdjs.RuntimeBehavior);
}

View File

@@ -20,6 +20,27 @@ namespace gdjs {
const getGlobalResourceNames = (projectData: ProjectData): Array<string> =>
projectData.usedResources.map((resource) => resource.name);
let supportedCompressionMethods: ('cs:gzip' | 'cs:deflate')[] | null = null;
const getSupportedCompressionMethods = (): ('cs:gzip' | 'cs:deflate')[] => {
if (!!supportedCompressionMethods) {
return supportedCompressionMethods;
}
supportedCompressionMethods = [];
try {
// @ts-ignore - We are checking if the CompressionStream is available.
new CompressionStream('gzip');
supportedCompressionMethods.push('cs:gzip');
} catch (e) {}
try {
// @ts-ignore - We are checking if the CompressionStream is available.
new CompressionStream('deflate');
supportedCompressionMethods.push('cs:deflate');
} catch (e) {}
return supportedCompressionMethods;
};
/** Options given to the game at startup. */
export type RuntimeGameOptions = {
/** if true, force fullscreen. */
@@ -32,6 +53,8 @@ namespace gdjs {
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without events. */
projectDataOnlyExport?: boolean;
/** if true, preview is launched from GDevelop native mobile app. */
nativeMobileApp?: boolean;
/** The address of the debugger server, to reach out using WebSocket. */
websocketDebuggerServerAddress?: string;
/** The port of the debugger server, to reach out using WebSocket. */
@@ -62,6 +85,14 @@ namespace gdjs {
*/
allowAuthenticationUsingIframeForPreview?: boolean;
/**
* If set, this data is used to authenticate automatically when launching the game.
* This is only useful during previews.
*/
playerUsername?: string;
playerId?: string;
playerToken?: string;
/**
* If set, the game should use the specified environment for making calls
* to GDevelop APIs ("dev" = development APIs).
@@ -267,7 +298,7 @@ namespace gdjs {
* Return the additional options passed to the RuntimeGame when created.
* @returns The additional options, if any.
*/
getAdditionalOptions(): RuntimeGameOptions | null {
getAdditionalOptions(): RuntimeGameOptions {
return this._options;
}
@@ -900,6 +931,26 @@ namespace gdjs {
}
}
/**
* Helper function to get information about the platform running the game.
*/
getPlatformInfo = () => {
return {
// @ts-ignore
isCordova: !!window.cordova,
devicePlatform:
// @ts-ignore
typeof device !== 'undefined' ? device.platform || '' : '',
navigatorPlatform:
typeof navigator !== 'undefined' ? navigator.platform : '',
hasTouch:
typeof navigator !== 'undefined'
? !!navigator.maxTouchPoints && navigator.maxTouchPoints > 2
: false,
supportedCompressionMethods: getSupportedCompressionMethods(),
};
};
_setupGameVisibilityEvents() {
if (typeof navigator !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
@@ -954,6 +1005,7 @@ namespace gdjs {
* either in sendedDuration or notYetSentDuration.
**/
let lastSessionResumeTime = Date.now();
const platform = this.getPlatformInfo();
fetch(baseUrl + '/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -970,17 +1022,10 @@ namespace gdjs {
location: window.location.href,
},
platform: {
// @ts-ignore
isCordova: !!window.cordova,
devicePlatform:
// @ts-ignore
typeof device !== 'undefined' ? device.platform || '' : '',
navigatorPlatform:
typeof navigator !== 'undefined' ? navigator.platform : '',
hasTouch:
typeof navigator !== 'undefined'
? !!navigator.maxTouchPoints && navigator.maxTouchPoints > 2
: false,
isCordova: platform.isCordova,
devicePlatform: platform.devicePlatform,
navigatorPlatform: platform.navigatorPlatform,
hasTouch: platform.hasTouch,
},
}),
})
@@ -1217,11 +1262,38 @@ namespace gdjs {
: [];
}
getNetworkSyncData(): GameNetworkSyncData {
return {
var: this._variables.getNetworkSyncData(),
ss: this._sceneStack.getNetworkSyncData(),
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): GameNetworkSyncData | null {
const syncData: GameNetworkSyncData = {
var: this._variables.getNetworkSyncData(syncOptions),
ss: this._sceneStack.getNetworkSyncData(syncOptions) || undefined,
};
const extensionsVariablesSyncData = {};
this._variablesByExtensionName.forEach((variables, extensionName) => {
const extensionVariablesSyncData = variables.getNetworkSyncData(
syncOptions
);
// If there is no variables to sync, don't include the extension in the sync data.
if (extensionVariablesSyncData.length) {
extensionsVariablesSyncData[
extensionName
] = extensionVariablesSyncData;
}
});
syncData.extVar = extensionsVariablesSyncData;
if (
(!syncData.var || syncData.var.length === 0) &&
!syncData.ss &&
(!syncData.extVar || Object.keys(syncData.extVar).length === 0)
) {
// Nothing to sync.
return null;
}
return syncData;
}
updateFromNetworkSyncData(syncData: GameNetworkSyncData) {
@@ -1231,6 +1303,22 @@ namespace gdjs {
if (syncData.ss) {
this._sceneStack.updateFromNetworkSyncData(syncData.ss);
}
if (syncData.extVar) {
for (const extensionName in syncData.extVar) {
if (!syncData.extVar.hasOwnProperty(extensionName)) {
continue;
}
const extensionVariablesData = syncData.extVar[extensionName];
const extensionVariables = this.getVariablesForExtension(
extensionName
);
if (extensionVariables) {
extensionVariables.updateFromNetworkSyncData(
extensionVariablesData
);
}
}
}
}
}
}

View File

@@ -453,17 +453,22 @@ namespace gdjs {
* This can be redefined by objects to send more information.
* @returns The full network sync data.
*/
getObjectNetworkSyncData(): ObjectNetworkSyncData {
getNetworkSyncData(): ObjectNetworkSyncData {
const behaviorNetworkSyncData = {};
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
const behavior = this._behaviors[i];
this._behaviors.forEach((behavior) => {
if (!behavior.isSyncedOverNetwork()) {
return;
}
const networkSyncData = behavior.getNetworkSyncData();
if (networkSyncData) {
behaviorNetworkSyncData[behavior.getName()] = networkSyncData;
}
}
});
const variablesNetworkSyncData = this._variables.getNetworkSyncData();
const variablesNetworkSyncData = this._variables.getNetworkSyncData({
// No need to send the player number, as the owner of the object syncs all its variables.
});
const effectsNetworkSyncData = {};
for (const effectName in this._rendererEffects) {
@@ -485,6 +490,7 @@ namespace gdjs {
zo: this.zOrder,
a: this.angle,
hid: this.hidden,
lay: this.layer,
if: this._instantForces.map((force) => force.getNetworkSyncData()),
pfx: this._permanentForceX,
pfy: this._permanentForceY,
@@ -502,7 +508,7 @@ namespace gdjs {
* @param networkSyncData The new data for the object.
* @returns true if the object was updated, false if it could not (i.e: network sync is not supported).
*/
updateFromObjectNetworkSyncData(networkSyncData: ObjectNetworkSyncData) {
updateFromNetworkSyncData(networkSyncData: ObjectNetworkSyncData) {
if (networkSyncData.x !== undefined) {
this.setX(networkSyncData.x);
}
@@ -522,13 +528,24 @@ namespace gdjs {
this.hide(networkSyncData.hid);
}
if (
networkSyncData.lay !== undefined &&
this.layer !== networkSyncData.lay
) {
this.setLayer(networkSyncData.lay);
}
if (networkSyncData.if) {
// Force clear all forces and reapply them, using the garbage collector to recycle forces.
// Is that efficient?
this.clearForces();
for (let i = 0, len = networkSyncData.if.length; i < len; ++i) {
const forceData = networkSyncData.if[i];
const recycledOrNewForce = RuntimeObject.forcesGarbage.pop() as gdjs.Force;
const recycledOrNewForce = this._getRecycledForce(
forceData.x,
forceData.y,
forceData.m
);
recycledOrNewForce.updateFromNetworkSyncData(forceData);
this._instantForces.push(recycledOrNewForce);
}

View File

@@ -772,10 +772,43 @@ namespace gdjs {
return this._isJustResumed;
}
getNetworkSyncData(): LayoutNetworkSyncData {
const variablesNetworkSyncData = this._variables.getNetworkSyncData();
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): LayoutNetworkSyncData | null {
const syncedPlayerNumber = syncOptions.playerNumber;
const variablesNetworkSyncData = this._variables.getNetworkSyncData(
syncOptions
);
const extensionsVariablesSyncData = {};
this._variablesByExtensionName.forEach((variables, extensionName) => {
const extensionVariablesSyncData = variables.getNetworkSyncData(
syncOptions
);
// If there is no variables to sync, don't include the extension in the sync data.
if (extensionVariablesSyncData) {
extensionsVariablesSyncData[
extensionName
] = extensionVariablesSyncData;
}
});
if (
syncedPlayerNumber !== undefined &&
syncedPlayerNumber !== 1 &&
(!this.networkId ||
(variablesNetworkSyncData.length === 0 &&
!Object.keys(extensionsVariablesSyncData).length))
) {
// If we are getting sync data for a specific player,
// and they are not the host, there is no sync data to send if:
// - The scene has no networkId (it's either not a multiplayer scene or the scene is not yet networked).
// - There are no variables to sync in the scene or extensions.
return null;
}
return {
var: variablesNetworkSyncData,
extVar: extensionsVariablesSyncData,
id: this.getOrCreateNetworkId(),
};
}
@@ -784,6 +817,22 @@ namespace gdjs {
if (syncData.var) {
this._variables.updateFromNetworkSyncData(syncData.var);
}
if (syncData.extVar) {
for (const extensionName in syncData.extVar) {
if (!syncData.extVar.hasOwnProperty(extensionName)) {
continue;
}
const extensionVariablesData = syncData.extVar[extensionName];
const extensionVariables = this._variablesByExtensionName.get(
extensionName
);
if (extensionVariables) {
extensionVariables.updateFromNetworkSyncData(
extensionVariablesData
);
}
}
}
}
getOrCreateNetworkId(): string {

View File

@@ -1,5 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Scene stack');
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
/**
* Hold the stack of scenes ({@link gdjs.RuntimeScene}) being played.
@@ -197,8 +198,17 @@ namespace gdjs {
return this._wasFirstSceneLoaded;
}
getNetworkSyncData(): SceneStackNetworkSyncData {
// If this method is called, we are the host, so we can take charge of
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SceneStackNetworkSyncData | null {
const syncedPlayerNumber = syncOptions.playerNumber;
if (syncedPlayerNumber !== undefined && syncedPlayerNumber !== 1) {
// If we are getting sync data of a specific player,
// and they are not the host, we don't sync the scene stack.
return null;
}
// If we are the host, we can take charge of
// generating a networkId for each scene if they don't have one.
// They will be reconciled on the other players' games.
const sceneStackSyncData: SceneStackSceneNetworkSyncData[] = [];
@@ -238,7 +248,7 @@ namespace gdjs {
const sceneSyncData = sceneStackSyncData[i];
const sceneAtThisPositionInOurStack = this._stack[i];
if (!sceneAtThisPositionInOurStack) {
logger.info(
debugLogger.info(
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
);
// We have less scenes in the stack than the host, let's add the scene.
@@ -251,7 +261,7 @@ namespace gdjs {
}
if (sceneAtThisPositionInOurStack.getName() !== sceneSyncData.name) {
logger.info(
debugLogger.info(
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} is not the same as the expected ${
sceneSyncData.name
}, replacing.`
@@ -274,7 +284,7 @@ namespace gdjs {
sceneSyncData.networkId &&
sceneSyncData.name === sceneAtThisPositionInOurStack.getName()
) {
logger.info(
debugLogger.info(
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} has no networkId, let's assume it's the right one and reconcile it with the id ${
sceneSyncData.networkId
}.`
@@ -291,7 +301,7 @@ namespace gdjs {
if (
sceneAtThisPositionInOurStack.networkId !== sceneSyncData.networkId
) {
logger.info(
debugLogger.info(
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} has a different networkId ${
sceneAtThisPositionInOurStack.networkId
} than the expected ${sceneSyncData.networkId}, replacing.`

View File

@@ -107,9 +107,9 @@ namespace gdjs {
return true;
}
getObjectNetworkSyncData(): SpriteNetworkSyncData {
getNetworkSyncData(): SpriteNetworkSyncData {
return {
...super.getObjectNetworkSyncData(),
...super.getNetworkSyncData(),
anim: this._animator.getNetworkSyncData(),
ifx: this.isFlippedX(),
ify: this.isFlippedY(),
@@ -120,8 +120,8 @@ namespace gdjs {
};
}
updateFromObjectNetworkSyncData(newNetworkSyncData: SpriteNetworkSyncData) {
super.updateFromObjectNetworkSyncData(newNetworkSyncData);
updateFromNetworkSyncData(newNetworkSyncData: SpriteNetworkSyncData) {
super.updateFromNetworkSyncData(newNetworkSyncData);
if (newNetworkSyncData.ifx !== undefined) {
this.flipX(newNetworkSyncData.ifx);
}
@@ -138,7 +138,9 @@ namespace gdjs {
this.setOpacity(newNetworkSyncData.op);
}
if (newNetworkSyncData.anim) {
this._animator.updateFromObjectNetworkSyncData(newNetworkSyncData.anim);
this._animator.updateFromNetworkSyncData(newNetworkSyncData.anim);
// TODO: optimize updating the animation frame only if needed.
this._updateAnimationFrame();
}
if (
newNetworkSyncData.ifx !== undefined ||

View File

@@ -39,20 +39,24 @@ declare type ObjectData = {
effects: Array<EffectData>;
};
declare type GetNetworkSyncDataOptions = { playerNumber?: number };
/** Object containing basic properties for all objects synchronizing over the network. */
declare type BasicObjectNetworkSyncData = {
/** The position of the object on the X axis. */
/** The position of the instance on the X axis. */
x: number;
/** The position of the object on the Y axis. */
/** The position of the instance on the Y axis. */
y: number;
/** The position of the object on the Z axis. Defined only for 3D games */
/** The position of the instance on the Z axis. Defined only for 3D games */
z?: number;
/** Z order of the object */
/** Z order of the instance */
zo: number;
/** The angle of the object. */
/** The angle of the instance. */
a: number;
/** If the object is hidden */
/** If the instance is hidden */
hid: boolean;
/** The layer where the instance lives */
lay: string;
/** All the instant forces */
if: Array<ForceNetworkSyncData>;
/** Permanent force on X */
@@ -122,6 +126,7 @@ declare type VariableNetworkSyncData = {
value: string | float | boolean;
children?: VariableNetworkSyncData[];
type: VariableType;
owner: number;
};
/** Properties to set up a behavior. */
@@ -163,6 +168,9 @@ declare interface LayoutData {
declare interface LayoutNetworkSyncData {
id: string;
var?: VariableNetworkSyncData[];
extVar?: {
[extensionName: string]: VariableNetworkSyncData[];
};
}
declare interface SceneStackSceneNetworkSyncData {
@@ -175,6 +183,9 @@ declare type SceneStackNetworkSyncData = SceneStackSceneNetworkSyncData[];
declare interface GameNetworkSyncData {
var?: VariableNetworkSyncData[];
ss?: SceneStackNetworkSyncData;
extVar?: {
[extensionName: string]: VariableNetworkSyncData[];
};
}
declare interface EventsFunctionsExtensionData {

View File

@@ -24,6 +24,11 @@ namespace gdjs {
_childrenArray: gdjs.Variable[] = [];
_undefinedInContainer: boolean = false;
// When synchronised over the network, this defines which player is the owner of the variable.
// Default is 0, meaning that the variable is owned by the host.
// If null, the variable is not synchronised over the network.
_playerNumber: number | null = 0;
/**
* @param [varData] The optional initial content of the variable.
*/
@@ -628,5 +633,17 @@ namespace gdjs {
})
);
}
getPlayerOwnership(): number | null {
return this._playerNumber;
}
setPlayerOwnership(playerNumber: number) {
this._playerNumber = playerNumber;
}
disableSynchronization() {
this._playerNumber = null;
}
}
}

View File

@@ -192,15 +192,58 @@ namespace gdjs {
const variable = this._variables.get(name);
return !!variable && !variable.isUndefinedInContainer();
}
/**
* Check if a variable exists in the container.
* @param variable The variable
* @return true if the variable exists.
*/
hasVariable(variable: gdjs.Variable): boolean {
const foundVariable = this._variablesArray.find((v) => v === variable);
return !!foundVariable && !foundVariable.isUndefinedInContainer();
}
getVariableNameInContainerByLoopingThroughAllVariables(
variable: gdjs.Variable
): string | null {
const variableItems = this._variables.items;
for (const variableName in variableItems) {
if (variableItems.hasOwnProperty(variableName)) {
if (variableItems[variableName] === variable) {
return variableName;
}
}
}
return null;
}
static _deletedVars: Array<string | undefined> = [];
getNetworkSyncData(): VariableNetworkSyncData[] {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): VariableNetworkSyncData[] {
const syncedPlayerNumber = syncOptions.playerNumber;
const networkSyncData: VariableNetworkSyncData[] = [];
const variableNames = [];
this._variables.keys(variableNames);
variableNames.forEach((variableName) => {
const variable = this._variables.get(variableName);
if (variable.isUndefinedInContainer()) {
const variableOwner = variable.getPlayerOwnership();
if (
// Variable undefined.
variable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Owned by host but we are not player 1.
variableOwner === 0 &&
syncedPlayerNumber !== 1) ||
// Owned by a player but we are not this player.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
) {
// In those cases, the variable should not be synchronized.
return;
}
@@ -215,6 +258,7 @@ namespace gdjs {
value: variableValue,
type: variableType,
children: this.getStructureNetworkSyncData(variable),
owner: variableOwner,
});
});
@@ -227,46 +271,71 @@ namespace gdjs {
variable: gdjs.Variable
): VariableNetworkSyncData[] | undefined {
if (variable.getType() === 'array') {
return variable.getAllChildrenArray().map((childVariable) => {
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
variable.getAllChildrenArray().forEach((childVariable) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
return {
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
allVariableNetworkSyncData.push({
name: '',
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
};
owner: childVariableOwner,
});
});
return allVariableNetworkSyncData;
}
if (variable.getType() === 'structure') {
const variableChildren = variable.getAllChildren();
if (!variableChildren) return undefined;
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
const childrenSyncData = variableChildren
? Object.entries(variableChildren).map(
([childVariableName, childVariable]) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' ||
childVariableType === 'array'
? ''
: childVariable.getValue();
Object.entries(variableChildren).forEach(
([childVariableName, childVariable]) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
return {
name: childVariableName,
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
};
}
)
: undefined;
allVariableNetworkSyncData.push({
name: childVariableName,
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
owner: childVariableOwner,
});
}
);
return childrenSyncData;
return allVariableNetworkSyncData;
}
return undefined;
@@ -276,19 +345,56 @@ namespace gdjs {
const that = this;
for (let j = 0; j < networkSyncData.length; ++j) {
const variableSyncData = networkSyncData[j];
const variableName = variableSyncData.name;
const variableData = that._getVariableDataFromNetworkSyncData(
variableSyncData
);
const variableName = variableData.name;
if (!variableName) continue;
const variable = that.get(variableName);
variable.reinitialize({
name: variableName,
value: variableSyncData.value,
type: variableSyncData.type,
children: variableSyncData.children,
});
// // If we receive an update for this variable for a different owner than the one we know about,
// then 2 cases:
// - If we are the owner of the variable, then ignore the message, we assume it's a late update message or a wrong one,
// we are confident that we own this variable. (it may be reverted if we don't receive an acknowledgment in time)
// - If we are not the owner of the variable, then assume that we missed the ownership change message, so update the variable's
// ownership and then update the variable.
const syncedVariableOwner = variableSyncData.owner;
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const currentVariableOwner = variable.getPlayerOwnership();
if (currentPlayerNumber === currentVariableOwner) {
console.info(
`Variable ${variableName} is owned by us ${gdjs.multiplayer.playerNumber}, ignoring update message from ${syncedVariableOwner}.`
);
return;
}
if (syncedVariableOwner !== currentVariableOwner) {
console.info(
`Variable ${variableName} is owned by ${currentVariableOwner} on our game, changing ownership to ${syncedVariableOwner} as part of the update event.`
);
variable.setPlayerOwnership(syncedVariableOwner);
}
variable.reinitialize(variableData);
}
}
_getVariableDataFromNetworkSyncData(
syncData: VariableNetworkSyncData
): VariableData {
return {
name: syncData.name,
value: syncData.value,
type: syncData.type,
children: syncData.children
? syncData.children.map((childSyncData) =>
this._getVariableDataFromNetworkSyncData(childSyncData)
)
: undefined,
};
}
/**
* "Bad" variable container, used by events when no other valid container can be found.
* This container has no state and always returns the bad variable ( see VariablesContainer.badVariable ).
@@ -327,6 +433,15 @@ namespace gdjs {
getStructureNetworkSyncData: function () {
return undefined;
},
_getVariableDataFromNetworkSyncData: function () {
return {};
},
hasVariable: function () {
return false;
},
getVariableNameInContainerByLoopingThroughAllVariables: function () {
return '';
},
};
/**
@@ -342,6 +457,7 @@ namespace gdjs {
_str: '',
_undefinedInContainer: true,
_value: 0,
_playerNumber: 0,
fromJSON: () => gdjs.VariablesContainer.badVariable,
toJSObject: () => 0,
fromJSObject: () => gdjs.VariablesContainer.badVariable,
@@ -433,6 +549,15 @@ namespace gdjs {
isUndefinedInContainer: function () {
return true;
},
getPlayerOwnership: function () {
return 0;
},
setPlayerOwnership: function () {
return;
},
disableSynchronization: function () {
return;
},
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -111,6 +111,7 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlayerAuthentication/playerauthenticationtools.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlayerAuthentication/playerauthenticationcomponents.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/messageManager.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayerVariablesManager.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayertools.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayercomponents.js',
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayerobjectruntimebehavior.js',

View File

@@ -3566,9 +3566,11 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions UseWindowMessageDebuggerClient();
[Ref] PreviewExportOptions SetLayoutName([Const] DOMString layoutName);
[Ref] PreviewExportOptions SetFallbackAuthor([Const] DOMString id, [Const] DOMString username);
[Ref] PreviewExportOptions SetAuthenticatedPlayer([Const] DOMString playerId, [Const] DOMString playerUsername, [Const] DOMString playerToken);
[Ref] PreviewExportOptions SetExternalLayoutName([Const] DOMString externalLayoutName);
[Ref] PreviewExportOptions SetIncludeFileHash([Const] DOMString includeFile, long hash);
[Ref] PreviewExportOptions SetProjectDataOnlyExport(boolean enable);
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);

View File

@@ -23,6 +23,15 @@ describe('libGD.js - GDJS Async Code Generation integration tests', function ()
});
const generateAndRunEventsForLayout = (events, logCode = false) => {
const { runtimeScene, runCompiledEvents } = generateEventsForLayout(
events,
logCode
);
runCompiledEvents();
return runtimeScene;
};
const generateEventsForLayout = (events, logCode = false) => {
const serializedProjectElement = new gd.SerializerElement();
project.serializeTo(serializedProjectElement);
@@ -46,8 +55,10 @@ describe('libGD.js - GDJS Async Code Generation integration tests', function ()
serializedProjectElement.delete();
serializedSceneElement.delete();
runCompiledEvents(gdjs, runtimeScene, []);
return runtimeScene;
return {
runtimeScene,
runCompiledEvents: () => runCompiledEvents(gdjs, runtimeScene, []),
};
};
describe('Basics', () => {
@@ -364,6 +375,77 @@ describe('libGD.js - GDJS Async Code Generation integration tests', function ()
).toBe(2 + 5);
});
it('can execute async events without side effect on local variables of the scene', function () {
// Try to reproduce a bug where the async events were not clearing
// the local variable stack.
// Local variables declarations were added over a not empty stack
// whereas actions, conditions and expressions were still using
// the expected stack index.
// The following comments apply to the second run of events.
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
const { runtimeScene, runCompiledEvents } = generateEventsForLayout([
{
type: 'BuiltinCommonInstructions::Standard',
// Expected: Define local variables at stack index 0.
// Actual: Define local variables at stack index 1.
variables: [{ name: 'MyLocalVariable', type: 'number', value: 0 }],
conditions: [],
actions: [
// Modify local variables at stack index 0.
{
type: { value: 'SetNumberVariable' },
parameters: ['MyLocalVariable', '=', '456'],
},
],
},
// Expected: Pop local variables at stack index 0.
// Actual: Pop local variables at stack index 1.
{
type: 'BuiltinCommonInstructions::Standard',
// Expected: Define local variables at stack index 0.
// Actual: Define local variables at stack index 1.
variables: [{ name: 'MyLocalVariable', type: 'number', value: 123 }],
conditions: [],
actions: [
// Get local variables at stack index 0.
// Expected : The declaration value
// Actual : The value set by the previous event: 456
{
type: { value: 'SetNumberVariable' },
parameters: ['SuccessVariable', '=', 'MyLocalVariable'],
},
// The only purpose of the wait is to trigger context switches.
{
type: { value: 'Wait' },
parameters: ['1'],
},
],
},
]);
// Run scene events a first time.
runCompiledEvents();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(123);
// Process the tasks (after faking it's finished).
// The context switching happens here.
runtimeScene.getAsyncTasksManager().markAllFakeAsyncTasksAsFinished();
runtimeScene.getAsyncTasksManager().processTasks(runtimeScene);
// This test can't actually reproduce the issue because
// `runCompiledEvents()` instantiate `gdjs.SceneCode.localVariables`
// at every call.
// Run scene events a second time.
runCompiledEvents();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(123);
});
it('generates an async fork that shares a scene variable a non-async sub-event', function () {
scene.getVariables().insertNew('SuccessVariable', 0).setValue(0);
scene.getVariables().insertNew('MySceneVariable', 0).setValue(1);

View File

@@ -184,46 +184,6 @@ describe('MetadataDeclarationHelper', () => {
project.delete();
});
it('can create metadata for free ExpressionAndConditions without description', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventFunction = eventExtension.insertNewEventsFunction('Value', 0);
eventFunction.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventExtension,
eventFunction
);
metadataDeclarationHelper.delete();
expect(extension.getAllExpressions().has('Value')).toBe(true);
const expression = extension.getAllExpressions().get('Value');
expect(expression.getFullName()).toBe('Value');
expect(expression.getDescription()).toBe('Return .');
expect(extension.getAllConditions().has('Value')).toBe(true);
const condition = extension.getAllConditions().get('Value');
expect(condition.getFullName()).toBe('Value');
// TODO The full name could be used when the description is not set.
expect(condition.getDescription()).toBe('Compare .');
// TODO The full name could be used when the sentence is not set.
expect(condition.getSentence()).toBe(' _PARAM1_ _PARAM2_');
extension.delete();
project.delete();
});
it('can create metadata for free ActionWithOperator', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
@@ -608,10 +568,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SetPropertyValue');
expect(action.getFullName()).toBe('Some value property');
expect(action.getDescription()).toBe(
'Change the property value for the some value.'
'Change the property value for Value.'
);
expect(action.getSentence()).toBe(
'Change the property value for the some value of _PARAM0_: _PARAM2_ _PARAM3_'
'Change the property value for Value of _PARAM0_: _PARAM2_ _PARAM3_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -631,11 +591,11 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::PropertyValue');
expect(condition.getFullName()).toBe('Some value property');
expect(condition.getDescription()).toBe(
'Compare the property value for the some value.'
'Compare the property value for Value.'
);
// The IDE fixes the first letter case.
expect(condition.getSentence()).toBe(
'The property value for the some value of _PARAM0_ _PARAM2_ _PARAM3_'
'The property value for Value of _PARAM0_ _PARAM2_ _PARAM3_'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -655,7 +615,7 @@ describe('MetadataDeclarationHelper', () => {
.get('PropertyValue');
expect(expression.getFullName()).toBe('Some value property');
expect(expression.getDescription()).toBe(
'Return the property value for the some value.'
'Return the property value for Value.'
);
expect(expression.isPrivate()).toBe(true);
@@ -786,10 +746,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SetPropertyValue');
expect(action.getFullName()).toBe('Some value property');
expect(action.getDescription()).toBe(
'Update the property value for "some value".'
'Update the property value for Value.'
);
expect(action.getSentence()).toBe(
'Set property value for some value of _PARAM0_ to _PARAM2_'
'Set property value for Value of _PARAM0_ to _PARAM2_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -808,10 +768,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::PropertyValue');
expect(condition.getFullName()).toBe('Some value property');
expect(condition.getDescription()).toBe(
'Check the property value for some value.'
'Check the property value for Value.'
);
expect(condition.getSentence()).toBe(
'Property some value of _PARAM0_ is true'
'Property Value of _PARAM0_ is true'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -863,10 +823,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SetSharedPropertyValue');
expect(action.getFullName()).toBe('Some value shared property');
expect(action.getDescription()).toBe(
'Change the property value for the some value.'
'Change the property value for Value.'
);
expect(action.getSentence()).toBe(
'Change the property value for the some value of _PARAM0_: _PARAM2_ _PARAM3_'
'Change the property value for Value of _PARAM0_: _PARAM2_ _PARAM3_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -886,11 +846,11 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SharedPropertyValue');
expect(condition.getFullName()).toBe('Some value shared property');
expect(condition.getDescription()).toBe(
'Compare the property value for the some value.'
'Compare the property value for Value.'
);
// The IDE fixes the first letter case.
expect(condition.getSentence()).toBe(
'The property value for the some value of _PARAM0_ _PARAM2_ _PARAM3_'
'The property value for Value of _PARAM0_ _PARAM2_ _PARAM3_'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -910,7 +870,7 @@ describe('MetadataDeclarationHelper', () => {
.get('SharedPropertyValue');
expect(expression.getFullName()).toBe('Some value shared property');
expect(expression.getDescription()).toBe(
'Return the property value for the some value.'
'Return the property value for Value.'
);
expect(expression.isPrivate()).toBe(true);
@@ -962,10 +922,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SetSharedPropertyValue');
expect(action.getFullName()).toBe('Some value shared property');
expect(action.getDescription()).toBe(
'Update the property value for "some value".'
'Update the property value for Value.'
);
expect(action.getSentence()).toBe(
'Set property value for some value of _PARAM0_ to _PARAM2_'
'Set property value for Value of _PARAM0_ to _PARAM2_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -984,10 +944,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyBehavior::SharedPropertyValue');
expect(condition.getFullName()).toBe('Some value shared property');
expect(condition.getDescription()).toBe(
'Check the property value for some value.'
'Check the property value for Value.'
);
expect(condition.getSentence()).toBe(
'Property some value of _PARAM0_ is true'
'Property Value of _PARAM0_ is true'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -1473,10 +1433,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyObject::SetPropertyValue');
expect(action.getFullName()).toBe('Some value property');
expect(action.getDescription()).toBe(
'Change the property value for the some value.'
'Change the property value for Value.'
);
expect(action.getSentence()).toBe(
'Change the property value for the some value of _PARAM0_: _PARAM1_ _PARAM2_'
'Change the property value for Value of _PARAM0_: _PARAM1_ _PARAM2_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -1495,11 +1455,11 @@ describe('MetadataDeclarationHelper', () => {
.get('MyObject::PropertyValue');
expect(condition.getFullName()).toBe('Some value property');
expect(condition.getDescription()).toBe(
'Compare the property value for the some value.'
'Compare the property value for Value.'
);
// The IDE fixes the first letter case.
expect(condition.getSentence()).toBe(
'The property value for the some value of _PARAM0_ _PARAM1_ _PARAM2_'
'The property value for Value of _PARAM0_ _PARAM1_ _PARAM2_'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -1518,7 +1478,7 @@ describe('MetadataDeclarationHelper', () => {
.get('PropertyValue');
expect(expression.getFullName()).toBe('Some value property');
expect(expression.getDescription()).toBe(
'Return the property value for the some value.'
'Return the property value for Value.'
);
expect(expression.isPrivate()).toBe(true);
@@ -1567,10 +1527,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyObject::SetPropertyValue');
expect(action.getFullName()).toBe('Some value property');
expect(action.getDescription()).toBe(
'Update the property value for "some value".'
'Update the property value for Value.'
);
expect(action.getSentence()).toBe(
'Set property value for some value of _PARAM0_ to _PARAM1_'
'Set property value for Value of _PARAM0_ to _PARAM1_'
);
expect(action.isHidden()).toBe(false);
expect(action.isPrivate()).toBe(true);
@@ -1588,10 +1548,10 @@ describe('MetadataDeclarationHelper', () => {
.get('MyObject::PropertyValue');
expect(condition.getFullName()).toBe('Some value property');
expect(condition.getDescription()).toBe(
'Check the property value for some value.'
'Check the property value for Value.'
);
expect(condition.getSentence()).toBe(
'Property some value of _PARAM0_ is true'
'Property Value of _PARAM0_ is true'
);
expect(condition.isHidden()).toBe(false);
expect(condition.isPrivate()).toBe(true);
@@ -1713,4 +1673,650 @@ describe('MetadataDeclarationHelper', () => {
});
});
});
it('can create metadata for free actions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventFunction = eventExtension.insertNewEventsFunction(
'MyFunction',
0
);
eventFunction.setFunctionType(gd.EventsFunction.Action);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventExtension,
eventFunction
);
metadataDeclarationHelper.delete();
expect(extension.getAllActions().has('MyFunction')).toBe(true);
const action = extension.getAllActions().get('MyFunction');
expect(action.getFullName()).toBe('MyFunction');
expect(action.getDescription()).toBe('MyFunction');
expect(action.getSentence()).toBe('MyFunction (Parameter: _PARAM1_)');
extension.delete();
project.delete();
});
it('can create metadata for free conditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventFunction = eventExtension.insertNewEventsFunction(
'MyFunction',
0
);
eventFunction.setFunctionType(gd.EventsFunction.Condition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventExtension,
eventFunction
);
metadataDeclarationHelper.delete();
expect(extension.getAllConditions().has('MyFunction')).toBe(true);
const condition = extension.getAllConditions().get('MyFunction');
expect(condition.getFullName()).toBe('MyFunction');
expect(condition.getDescription()).toBe('MyFunction');
expect(condition.getSentence()).toBe('MyFunction (Parameter: _PARAM1_)');
extension.delete();
project.delete();
});
it('can create metadata for free ExpressionAndConditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventFunction = eventExtension.insertNewEventsFunction('Value', 0);
eventFunction.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventExtension,
eventFunction
);
metadataDeclarationHelper.delete();
expect(extension.getAllExpressions().has('Value')).toBe(true);
const expression = extension.getAllExpressions().get('Value');
expect(expression.getFullName()).toBe('Value');
expect(expression.getDescription()).toBe('Return Value.');
expect(extension.getAllConditions().has('Value')).toBe(true);
const condition = extension.getAllConditions().get('Value');
expect(condition.getFullName()).toBe('Value');
expect(condition.getDescription()).toBe('Compare Value.');
expect(condition.getSentence()).toBe('Value (Parameter: _PARAM3_) _PARAM1_ _PARAM2_');
extension.delete();
project.delete();
});
it('can create metadata for free ActionWithOperator without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const getter = eventExtension.insertNewEventsFunction('Value', 0);
getter.setFunctionType(gd.EventsFunction.ExpressionAndConditions);
getter.setFullName('');
getter.setDescription('');
getter.setSentence('');
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
getter.getParameters().push_back(parameter);
const eventFunction = eventExtension.insertNewEventsFunction('SetValue', 0);
eventFunction.setFunctionType(gd.EventsFunction.ActionWithOperator);
eventFunction.setGetterName('Value');
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventExtension,
eventFunction
);
metadataDeclarationHelper.delete();
expect(extension.getAllActions().has('SetValue')).toBe(true);
const action = extension.getAllActions().get('SetValue');
expect(action.getFullName()).toBe('Value');
expect(action.getDescription()).toBe('Change Value');
expect(action.getSentence()).toBe('Change Value (Parameter: _PARAM3_): _PARAM1_ _PARAM2_');
extension.delete();
project.delete();
});
it('can create metadata for behavior actions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventBehavior = eventExtension
.getEventsBasedBehaviors()
.insertNew('MyBehavior', 0);
const eventFunction = eventBehavior
.getEventsFunctions()
.insertNewEventsFunction('MyFunction', 0);
eventFunction.setFunctionType(gd.EventsFunction.Action);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
eventExtension,
eventBehavior
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
extension,
eventExtension,
eventBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
expect(extension.getBehaviorsTypes().size()).toBe(1);
expect(extension.getBehaviorsTypes().at(0)).toBe('MyBehavior');
const behaviorMetadata = extension.getBehaviorMetadata('MyBehavior');
expect(behaviorMetadata.getAllActions().has('MyBehavior::MyFunction')).toBe(
true
);
const action = behaviorMetadata
.getAllActions()
.get('MyBehavior::MyFunction');
expect(action.getFullName()).toBe('MyFunction');
expect(action.getDescription()).toBe('MyFunction');
expect(action.getSentence()).toBe('MyFunction (Object: _PARAM0_, Behavior: _PARAM1_, Parameter: _PARAM2_)');
expect(action.getParametersCount()).toBe(4);
checkBehaviorDefaultParameters(action);
extension.delete();
project.delete();
});
it('can create metadata for behavior conditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventBehavior = eventExtension
.getEventsBasedBehaviors()
.insertNew('MyBehavior', 0);
const eventFunction = eventBehavior
.getEventsFunctions()
.insertNewEventsFunction('MyFunction', 0);
eventFunction.setFunctionType(gd.EventsFunction.Condition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
eventExtension,
eventBehavior
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
extension,
eventExtension,
eventBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
expect(extension.getBehaviorsTypes().size()).toBe(1);
expect(extension.getBehaviorsTypes().at(0)).toBe('MyBehavior');
const behaviorMetadata = extension.getBehaviorMetadata('MyBehavior');
expect(
behaviorMetadata.getAllConditions().has('MyBehavior::MyFunction')
).toBe(true);
const condition = behaviorMetadata
.getAllConditions()
.get('MyBehavior::MyFunction');
expect(condition.getFullName()).toBe('MyFunction');
expect(condition.getDescription()).toBe('MyFunction');
expect(condition.getSentence()).toBe('MyFunction (Object: _PARAM0_, Behavior: _PARAM1_, Parameter: _PARAM2_)');
expect(condition.getParametersCount()).toBe(4);
checkBehaviorDefaultParameters(condition);
extension.delete();
project.delete();
});
it('can create metadata for behavior ExpressionAndConditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventBehavior = eventExtension
.getEventsBasedBehaviors()
.insertNew('MyBehavior', 0);
const eventFunction = eventBehavior
.getEventsFunctions()
.insertNewEventsFunction('Value', 0);
eventFunction.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
eventExtension,
eventBehavior
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
extension,
eventExtension,
eventBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
expect(extension.getBehaviorsTypes().size()).toBe(1);
expect(extension.getBehaviorsTypes().at(0)).toBe('MyBehavior');
const behaviorMetadata = extension.getBehaviorMetadata('MyBehavior');
expect(behaviorMetadata.getAllExpressions().has('Value')).toBe(true);
const expression = behaviorMetadata.getAllExpressions().get('Value');
expect(expression.getFullName()).toBe('Value');
expect(expression.getDescription()).toBe('Return Value.');
expect(behaviorMetadata.getAllConditions().has('MyBehavior::Value')).toBe(
true
);
const condition = behaviorMetadata
.getAllConditions()
.get('MyBehavior::Value');
expect(condition.getFullName()).toBe('Value');
expect(condition.getDescription()).toBe('Compare Value.');
// The IDE fixes the first letter case.
expect(condition.getSentence()).toBe(
'Value (Parameter: _PARAM4_) of _PARAM0_ _PARAM2_ _PARAM3_'
);
extension.delete();
project.delete();
});
it('can create metadata for behavior ActionWithOperator without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventBehavior = eventExtension
.getEventsBasedBehaviors()
.insertNew('MyBehavior', 0);
const getter = eventBehavior
.getEventsFunctions()
.insertNewEventsFunction('Value', 0);
getter.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
getter.setFullName('');
getter.setDescription('');
getter.setSentence('');
const eventFunction = eventBehavior
.getEventsFunctions()
.insertNewEventsFunction('SetValue', 0);
eventFunction.setFunctionType(gd.EventsFunction.ActionWithOperator);
eventFunction.setGetterName('Value');
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
eventExtension,
eventBehavior
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
getter.getParameters().push_back(parameter);
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
extension,
eventExtension,
eventBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
expect(extension.getBehaviorsTypes().size()).toBe(1);
expect(extension.getBehaviorsTypes().at(0)).toBe('MyBehavior');
const behaviorMetadata = extension.getBehaviorMetadata('MyBehavior');
expect(behaviorMetadata.getAllActions().has('MyBehavior::SetValue')).toBe(
true
);
const action = behaviorMetadata.getAllActions().get('MyBehavior::SetValue');
expect(action.getFullName()).toBe('Value');
expect(action.getDescription()).toBe('Change Value');
expect(action.getSentence()).toBe(
'Change Value (Parameter: _PARAM4_) of _PARAM0_: _PARAM2_ _PARAM3_'
);
extension.delete();
project.delete();
});
it('can create metadata for object actions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventObject = eventExtension
.getEventsBasedObjects()
.insertNew('MyObject', 0);
const eventFunction = eventObject
.getEventsFunctions()
.insertNewEventsFunction('MyFunction', 0);
eventFunction.setFunctionType(gd.EventsFunction.Action);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureObjectEventsFunctionsProperParameters(
eventExtension,
eventObject
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const objectMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateObjectMetadata(
project,
extension,
eventExtension,
eventObject,
objectMethodMangledNames
);
objectMethodMangledNames.delete();
expect(extension.getExtensionObjectsTypes().size()).toBe(1);
expect(extension.getExtensionObjectsTypes().at(0)).toBe('MyObject');
const objectMetadata = extension.getObjectMetadata('MyObject');
expect(objectMetadata.getAllActions().has('MyObject::MyFunction')).toBe(
true
);
const action = objectMetadata.getAllActions().get('MyObject::MyFunction');
expect(action.getFullName()).toBe('MyFunction');
expect(action.getDescription()).toBe('MyFunction');
expect(action.getSentence()).toBe('MyFunction (Object: _PARAM0_, Parameter: _PARAM1_)');
extension.delete();
project.delete();
});
it('can create metadata for object conditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventObject = eventExtension
.getEventsBasedObjects()
.insertNew('MyObject', 0);
const eventFunction = eventObject
.getEventsFunctions()
.insertNewEventsFunction('MyFunction', 0);
eventFunction.setFunctionType(gd.EventsFunction.Condition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureObjectEventsFunctionsProperParameters(
eventExtension,
eventObject
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const objectMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateObjectMetadata(
project,
extension,
eventExtension,
eventObject,
objectMethodMangledNames
);
objectMethodMangledNames.delete();
expect(extension.getExtensionObjectsTypes().size()).toBe(1);
expect(extension.getExtensionObjectsTypes().at(0)).toBe('MyObject');
const objectMetadata = extension.getObjectMetadata('MyObject');
expect(objectMetadata.getAllConditions().has('MyObject::MyFunction')).toBe(
true
);
const condition = objectMetadata
.getAllConditions()
.get('MyObject::MyFunction');
expect(condition.getFullName()).toBe('MyFunction');
expect(condition.getDescription()).toBe('MyFunction');
expect(condition.getSentence()).toBe('MyFunction (Object: _PARAM0_, Parameter: _PARAM1_)');
extension.delete();
project.delete();
});
it('can create metadata for object ExpressionAndConditions without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventObject = eventExtension
.getEventsBasedObjects()
.insertNew('MyObject', 0);
const eventFunction = eventObject
.getEventsFunctions()
.insertNewEventsFunction('Value', 0);
eventFunction.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
eventFunction.setFullName('');
eventFunction.setDescription('');
eventFunction.setSentence('');
gd.WholeProjectRefactorer.ensureObjectEventsFunctionsProperParameters(
eventExtension,
eventObject
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
eventFunction.getParameters().push_back(parameter);
const objectMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateObjectMetadata(
project,
extension,
eventExtension,
eventObject,
objectMethodMangledNames
);
objectMethodMangledNames.delete();
expect(extension.getExtensionObjectsTypes().size()).toBe(1);
expect(extension.getExtensionObjectsTypes().at(0)).toBe('MyObject');
const objectMetadata = extension.getObjectMetadata('MyObject');
expect(objectMetadata.getAllExpressions().has('Value')).toBe(true);
const expression = objectMetadata.getAllExpressions().get('Value');
expect(expression.getFullName()).toBe('Value');
expect(expression.getDescription()).toBe('Return Value.');
expect(objectMetadata.getAllConditions().has('MyObject::Value')).toBe(true);
const condition = objectMetadata.getAllConditions().get('MyObject::Value');
expect(condition.getFullName()).toBe('Value');
expect(condition.getDescription()).toBe('Compare Value.');
// The IDE fixes the first letter case.
expect(condition.getSentence()).toBe(
'Value (Parameter: _PARAM3_) of _PARAM0_ _PARAM1_ _PARAM2_'
);
extension.delete();
project.delete();
});
it('can create metadata for object ActionWithOperator without full name', () => {
const extension = new gd.PlatformExtension();
const project = new gd.Project();
const eventExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventObject = eventExtension
.getEventsBasedObjects()
.insertNew('MyObject', 0);
const getter = eventObject
.getEventsFunctions()
.insertNewEventsFunction('Value', 0);
getter.setFunctionType(gd.EventsFunction.ExpressionAndCondition);
getter.setFullName('');
getter.setDescription('');
getter.setSentence('');
const eventFunction = eventObject
.getEventsFunctions()
.insertNewEventsFunction('SetValue', 0);
eventFunction.setFunctionType(gd.EventsFunction.ActionWithOperator);
eventFunction.setGetterName('Value');
gd.WholeProjectRefactorer.ensureObjectEventsFunctionsProperParameters(
eventExtension,
eventObject
);
const parameter = new gd.ParameterMetadata();
parameter.setType('number');
parameter.setName("Parameter");
getter.getParameters().push_back(parameter);
const objectMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateObjectMetadata(
project,
extension,
eventExtension,
eventObject,
objectMethodMangledNames
);
objectMethodMangledNames.delete();
expect(extension.getExtensionObjectsTypes().size()).toBe(1);
expect(extension.getExtensionObjectsTypes().at(0)).toBe('MyObject');
const objectMetadata = extension.getObjectMetadata('MyObject');
expect(objectMetadata.getAllActions().has('MyObject::SetValue')).toBe(true);
const action = objectMetadata.getAllActions().get('MyObject::SetValue');
expect(action.getFullName()).toBe('Value');
expect(action.getDescription()).toBe('Change Value');
expect(action.getSentence()).toBe(
'Change Value (Parameter: _PARAM3_) of _PARAM0_: _PARAM1_ _PARAM2_'
);
extension.delete();
project.delete();
});
});

View File

@@ -2729,9 +2729,11 @@ export class PreviewExportOptions extends EmscriptenObject {
useWindowMessageDebuggerClient(): PreviewExportOptions;
setLayoutName(layoutName: string): PreviewExportOptions;
setFallbackAuthor(id: string, username: string): PreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): PreviewExportOptions;
setExternalLayoutName(externalLayoutName: string): PreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): PreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): PreviewExportOptions;
setNativeMobileApp(enable: boolean): PreviewExportOptions;
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;

View File

@@ -5,9 +5,11 @@ declare class gdPreviewExportOptions {
useWindowMessageDebuggerClient(): gdPreviewExportOptions;
setLayoutName(layoutName: string): gdPreviewExportOptions;
setFallbackAuthor(id: string, username: string): gdPreviewExportOptions;
setAuthenticatedPlayer(playerId: string, playerUsername: string, playerToken: string): gdPreviewExportOptions;
setExternalLayoutName(externalLayoutName: string): gdPreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): gdPreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): gdPreviewExportOptions;
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.05 2.25C5.66527 2.25 1.30005 6.61522 1.30005 12C1.30005 17.3068 5.16195 21.75 10.15 21.75C10.5643 21.75 10.9 21.4142 10.9 21C10.9 20.9921 10.8999 20.9843 10.8997 20.9765C10.9044 20.8116 10.855 20.644 10.7476 20.5024L10.7447 20.4986L10.7275 20.475C10.7113 20.4525 10.6859 20.4169 10.6527 20.3683C10.5862 20.2713 10.4883 20.123 10.3695 19.9269C10.1316 19.5344 9.81063 18.9517 9.48855 18.2055C9.30174 17.7727 9.115 17.2858 8.94397 16.75H10C10.4142 16.75 10.75 16.4142 10.75 16C10.75 15.5858 10.4142 15.25 10 15.25H8.54497C8.33571 14.2722 8.19995 13.1822 8.19995 11.9999C8.19995 11.2063 8.26111 10.4544 8.36461 9.75H13.5585C13.6779 10.5907 13.75 11.5089 13.75 12.5C13.75 12.9142 14.0858 13.25 14.5 13.25C14.9142 13.25 15.25 12.9142 15.25 12.5C15.25 11.517 15.1839 10.5991 15.0725 9.75H19.0077C19.1972 10.5704 19.3 11.4852 19.3 12.5C19.3 12.9142 19.6358 13.25 20.05 13.25C20.4643 13.25 20.8 12.9142 20.8 12.5C20.8 9.09006 19.7385 6.50325 17.9332 4.77129C16.1348 3.04596 13.6827 2.25 11.05 2.25ZM8.66049 8.25C8.89434 7.30819 9.19135 6.48293 9.48855 5.79437C9.81063 5.04815 10.1316 4.46551 10.3695 4.07301C10.4432 3.95135 10.5089 3.84811 10.564 3.76408C10.7248 3.75474 10.8869 3.75 11.05 3.75C11.208 3.75 11.3647 3.75322 11.52 3.75964L11.5693 3.83921C11.6147 3.9122 11.6588 3.98297 11.6975 4.05062C11.9206 4.44049 12.2232 5.02758 12.5275 5.79773C12.7955 6.47634 13.0648 7.29687 13.2821 8.25H8.66049ZM14.8175 8.25C14.5712 7.07524 14.2476 6.06954 13.9226 5.24665C13.7347 4.77099 13.5463 4.35632 13.3719 4.00523C14.7414 4.32399 15.9433 4.94093 16.8948 5.85371C17.5558 6.48792 18.1135 7.28175 18.5249 8.25H14.8175ZM7.11905 8.25C7.38602 7.06669 7.7488 6.03994 8.11135 5.19995C8.28712 4.79273 8.46314 4.42865 8.62873 4.11103C6.48101 4.76942 4.70627 6.28078 3.69961 8.25H7.11905ZM3.1106 9.75C2.9083 10.4652 2.80005 11.22 2.80005 12C2.80005 13.1608 3.0095 14.2578 3.38654 15.25H7.01404C6.82147 14.2606 6.69995 13.1719 6.69995 11.9999C6.69995 11.2105 6.75508 10.4589 6.84992 9.75H3.1106ZM4.13147 16.75C5.20838 18.4661 6.8524 19.6874 8.74001 20.0984C8.54326 19.7347 8.3271 19.2998 8.11135 18.7999C7.85515 18.2063 7.59883 17.5195 7.37629 16.75H4.13147Z" fill="currentColor"/>
<path d="M22.4942 15.3819C22.3632 14.9889 21.9385 14.7766 21.5455 14.9075C21.1526 15.0385 20.9402 15.4632 21.0712 15.8562C21.3843 16.7958 21.2333 17.3532 21.0239 17.6528C20.8061 17.9644 20.4186 18.1536 19.9579 18.1536C19.4622 18.1536 19.1791 18.0254 18.9403 17.826C18.6567 17.5893 18.407 17.2324 18.0273 16.6628C17.6211 16.0535 17.1754 15.5874 16.6271 15.2828C16.0737 14.9753 15.4821 14.869 14.8487 14.869C13.1915 14.869 11.849 16.4885 12.2885 18.6857C12.3697 19.0919 12.7649 19.3553 13.171 19.2741C13.5772 19.1928 13.8406 18.7977 13.7594 18.3915C13.4689 16.9393 14.3162 16.369 14.8487 16.369C15.3101 16.369 15.6308 16.4452 15.8986 16.594C16.1713 16.7455 16.4555 17.0093 16.7792 17.4949L16.7899 17.5108C17.1369 18.0314 17.5024 18.5796 17.979 18.9775C18.5053 19.4168 19.1346 19.6536 19.9579 19.6536C20.8163 19.6536 21.7061 19.2953 22.2534 18.512C22.8091 17.7169 22.9109 16.632 22.4942 15.3819Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="24" height="24" viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5594 14.5532C19.9524 14.4222 20.3771 14.6346 20.5081 15.0276C20.9015 16.2079 20.8078 17.2404 20.2765 18.0006C19.7536 18.7489 18.9045 19.0898 18.0882 19.0898C17.307 19.0898 16.7067 18.8647 16.2049 18.4457C15.7532 18.0687 15.4078 17.5505 15.0843 17.0652L15.0724 17.0474C14.772 16.5968 14.5111 16.3563 14.2645 16.2193C14.0229 16.0851 13.7313 16.0147 13.3048 16.0147C12.8422 16.0147 12.0647 16.5151 12.3319 17.851C12.4131 18.2572 12.1497 18.6523 11.7435 18.7335C11.3374 18.8148 10.9423 18.5514 10.861 18.1452C10.4448 16.0644 11.7174 14.5147 13.3048 14.5147C13.9033 14.5147 14.4658 14.6152 14.993 14.9081C15.5152 15.1982 15.9376 15.641 16.3205 16.2154C16.6769 16.75 16.907 17.0779 17.1661 17.2942C17.3805 17.4732 17.6345 17.5898 18.0882 17.5898C18.5068 17.5898 18.8536 17.4181 19.047 17.1414C19.232 16.8766 19.3749 16.3716 19.0851 15.5019C18.9541 15.1089 19.1665 14.6842 19.5594 14.5532Z" fill="#000" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98529 18.5C8.98529 18.0858 8.64951 17.75 8.23529 17.75H7.57353V12.4265H18.3971L18.3971 6.73532C18.3971 5.28152 17.2186 4.10297 15.7647 4.10297H5.88235C4.42854 4.10297 3.25 5.28151 3.25 6.73532V16.6177C3.25 18.0715 4.42855 19.25 5.88235 19.25H8.23529C8.64951 19.25 8.98529 18.9142 8.98529 18.5ZM5.88235 5.60297C5.25697 5.60297 4.75 6.10994 4.75 6.73532V7.86768H6.07353V5.60297H5.88235ZM4.75 16.6177C4.75 17.2431 5.25696 17.75 5.88235 17.75H6.07353V15.4853H4.75V16.6177ZM16.8971 6.73532C16.8971 6.10993 16.3901 5.60297 15.7647 5.60297H15.5735V7.86768H16.8971V6.73532ZM4.75 13.9853V12.4265H6.07353V13.9853H4.75ZM6.07353 10.9265H4.75V9.36768H6.07353V10.9265ZM16.8971 9.36768V10.9265H15.5735V9.36768H16.8971ZM7.57353 10.9265H14.0735V5.60297H7.57353V10.9265Z" fill="#000" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,228 +0,0 @@
[
{
"id": "cameraParallax",
"titleByLocale": { "en": "Let's improve the camera and the background" },
"bulletPointsByLocale": [
{ "en": "Add a background with parallax effect" },
{ "en": "Add an extension" },
{ "en": "Use basic camera movements to follow the player" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/cameraParallax.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/cameraParallax/game.json",
"initialProjectData": {
"cameraScene": "CameraScene",
"player": "PlayerObject",
"farBackground": "FarBackground",
"midBackground": "MidBackground"
},
"isMiniTutorial": true
},
{
"id": "flingGame",
"titleByLocale": { "en": "Let's make a Fling Game" },
"bulletPointsByLocale": [
{ "en": "Learn to create a game from zero." },
{ "en": "Add a leaderboard to your game." }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/flingGame.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"isMiniTutorial": false
},
{
"id": "healthBar",
"titleByLocale": {
"en": "Let's communicate to the player the remaining health points"
},
"bulletPointsByLocale": [
{ "en": "Use a prefab for a health bar" },
{ "en": "Update the health bar based on the player's health" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/healthBar.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/healthBar/game.json",
"initialProjectData": { "level": "Level", "player": "Player" },
"isMiniTutorial": true
},
{
"id": "joystick",
"titleByLocale": { "en": "Let's add mobile controls to our game" },
"bulletPointsByLocale": [
{ "en": "Add a joystick prefab" },
{ "en": "Add a behavior" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/joystick.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/joystick/game.json",
"initialProjectData": {
"gameScene": "GameScene",
"ship": "OrangePlayerShip3"
},
"isMiniTutorial": true
},
{
"id": "knightPlatformer",
"titleByLocale": { "en": "Let's make a platformer game" },
"bulletPointsByLocale": [
{ "en": "Use a behavior to mark an object as a platform." },
{ "en": "Use another behavior to control the player." }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/knightPlatformer.json",
"availableLocales": ["en"],
"initialProjectData": {
"playerHitBox": "KnightHitBox",
"tiles": "Tiles",
"level1": "Level 1"
},
"isMiniTutorial": true
},
{
"id": "object3d",
"titleByLocale": { "en": "Let's add a 3D object to our game" },
"bulletPointsByLocale": [
{ "en": "Add a 3D Box" },
{ "en": "Add a behavior" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/object3d.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/object3d/game.json",
"initialProjectData": { "gameScene": "GameScene", "platform": "Platform" },
"isMiniTutorial": true
},
{
"id": "plinkoMultiplier",
"titleByLocale": { "en": "Let's improve a scoring system" },
"bulletPointsByLocale": [
{ "en": "Making objects disappear or appear when colliding" },
{ "en": "Creating, modifying and accessing a scene variable" },
{ "en": "Updating a score accordingly" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/plinkoMultiplier.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/plinkoMultiplier/game.json",
"initialProjectData": {
"gameScene": "GameScene",
"multiplier": "Multiplier",
"scoreMultiplier": "ScoreMultiplier",
"ball": "Ball",
"particles": "PegStar_Particle"
},
"isMiniTutorial": true
},
{
"id": "timer",
"titleByLocale": { "en": "Let's use time to measure a score" },
"bulletPointsByLocale": [
{ "en": "Create and modify a text" },
{ "en": "Start a timer" },
{ "en": "Use the timer to display a score" }
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/timer.json",
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/timer/game.json",
"initialProjectData": { "gameScene": "GameScene" },
"isMiniTutorial": true
}
]

View File

@@ -148,7 +148,10 @@ Remember that you can also [search for new features in the community extensions]
const generateExtensionHeaderText = ({ extension, depth }) => {
return {
text:
generateHeader({ headerName: extension.getFullName(), depth }).text +
generateHeader({
headerName: extension.getFullName() + (depth <= 1 ? ' Reference' : ''),
depth,
}).text +
`
${extension.getDescription()} ${generateReadMoreLink(extension.getHelpPath())}
`,

View File

@@ -756,9 +756,12 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
) => void
) => {
if (eventsBasedBehavior) {
this._onAddBehaviorEventsFunction(onAddEventsFunctionCb);
this._onAddBehaviorEventsFunction(
eventsBasedBehavior,
onAddEventsFunctionCb
);
} else if (eventsBasedObject) {
this._onAddObjectEventsFunction(onAddEventsFunctionCb);
this._onAddObjectEventsFunction(eventsBasedObject, onAddEventsFunctionCb);
} else {
this._onAddFreeEventsFunction(onAddEventsFunctionCb);
}
@@ -776,24 +779,32 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
};
_onAddBehaviorEventsFunction = (
eventsBasedBehavior: gdEventsBasedBehavior,
onAddEventsFunctionCb: (
parameters: ?EventsFunctionCreationParameters
) => void
) => {
this.setState({
behaviorMethodSelectorDialogOpen: true,
onAddEventsFunctionCb,
onAddEventsFunctionCb: parameters => {
onAddEventsFunctionCb(parameters);
this._onBehaviorEventsFunctionAdded(eventsBasedBehavior);
},
});
};
_onAddObjectEventsFunction = (
eventsBasedObject: gdEventsBasedObject,
onAddEventsFunctionCb: (
parameters: ?EventsFunctionCreationParameters
) => void
) => {
this.setState({
objectMethodSelectorDialogOpen: true,
onAddEventsFunctionCb,
onAddEventsFunctionCb: parameters => {
onAddEventsFunctionCb(parameters);
this._onObjectEventsFunctionAdded(eventsBasedObject);
},
});
};
@@ -827,9 +838,20 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
);
};
_onEventsFunctionAdded = (
selectedEventsFunction: gdEventsFunction,
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject
) => {
if (eventsBasedBehavior) {
this._onBehaviorEventsFunctionAdded(eventsBasedBehavior);
} else if (eventsBasedObject) {
this._onObjectEventsFunctionAdded(eventsBasedObject);
}
};
_onBehaviorEventsFunctionAdded = (
eventsBasedBehavior: gdEventsBasedBehavior,
eventsFunction: gdEventsFunction
eventsBasedBehavior: gdEventsBasedBehavior
) => {
// This will create the mandatory parameters for the newly added function.
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
@@ -838,10 +860,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
);
};
_onObjectEventsFunctionAdded = (
eventsBasedObject: gdEventsBasedObject,
eventsFunction: gdEventsFunction
) => {
_onObjectEventsFunctionAdded = (eventsBasedObject: gdEventsBasedObject) => {
// This will create the mandatory parameters for the newly added function.
gd.WholeProjectRefactorer.ensureObjectEventsFunctionsProperParameters(
this.props.eventsFunctionsExtension,
@@ -1341,7 +1360,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
onDeleteEventsFunction={this._onDeleteEventsFunction}
onRenameEventsFunction={this._makeRenameEventsFunction(i18n)}
onAddEventsFunction={this._onAddEventsFunction}
onEventsFunctionAdded={() => {}}
onEventsFunctionAdded={this._onEventsFunctionAdded}
// Behaviors
selectedEventsBasedBehavior={selectedEventsBasedBehavior}
onSelectEventsBasedBehavior={this._selectEventsBasedBehavior}

View File

@@ -57,7 +57,11 @@ export type EventsFunctionCallbacks = {|
eventsBasedObject: ?gdEventsBasedObject,
(parameters: ?EventsFunctionCreationParameters) => void
) => void,
onEventsFunctionAdded: (eventsFunction: gdEventsFunction) => void,
onEventsFunctionAdded: (
eventsFunction: gdEventsFunction,
eventsBasedBehavior: ?gdEventsBasedBehavior,
eventsBasedObject: ?gdEventsBasedObject
) => void,
|};
export type EventFunctionCommonProps = {|
@@ -429,7 +433,11 @@ export class EventsFunctionTreeViewItemContent implements TreeViewItemContent {
project
);
newEventsFunction.setName(newName);
this.props.onEventsFunctionAdded(newEventsFunction);
this.props.onEventsFunctionAdded(
newEventsFunction,
this.props.eventsBasedBehavior,
this.props.eventsBasedObject
);
this._onEventsFunctionModified();
this.props.onSelectEventsFunction(

View File

@@ -55,6 +55,7 @@ import { type GDevelopTheme } from '../UI/Theme';
const gd: libGDevelop = global.gd;
export const extensionConfigurationRootFolderId = 'extension-configuration';
export const extensionObjectsRootFolderId = 'extension-objects';
export const extensionBehaviorsRootFolderId = 'extension-behaviors';
export const extensionFunctionsRootFolderId = 'extension-functions';
@@ -676,7 +677,11 @@ const EventsFunctionsList = React.forwardRef<
scrollToItem(functionItemId);
}, 100); // A few ms is enough for a new render to be done.
onEventsFunctionAdded(eventsFunction);
onEventsFunctionAdded(
eventsFunction,
eventsBasedBehavior,
eventsBasedObject
);
if (unsavedChanges) {
unsavedChanges.triggerUnsavedChanges();
}
@@ -1035,7 +1040,7 @@ const EventsFunctionsList = React.forwardRef<
{
isRoot: true,
content: new LabelTreeViewItemContent(
extensionBehaviorsRootFolderId,
extensionConfigurationRootFolderId,
i18n._(t`Extension`)
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1053,7 +1058,7 @@ const EventsFunctionsList = React.forwardRef<
extensionGlobalVariablesItemId,
i18n._(t`Extension global variables`),
onSelectExtensionGlobalVariables,
'res/icons_default/publish_black.svg'
'res/icons_default/global_variable24_black.svg'
)
),
new LeafTreeViewItem(
@@ -1061,7 +1066,7 @@ const EventsFunctionsList = React.forwardRef<
extensionSceneVariablesItemId,
i18n._(t`Extension scene variables`),
onSelectExtensionSceneVariables,
'res/icons_default/scene_black.svg'
'res/icons_default/scene_variable24_black.svg'
)
),
];
@@ -1252,6 +1257,7 @@ const EventsFunctionsList = React.forwardRef<
extensionObjectsRootFolderId,
extensionBehaviorsRootFolderId,
extensionFunctionsRootFolderId,
extensionConfigurationRootFolderId,
...objectTreeViewItems.map(item => item.content.getId()),
...behaviorTreeViewItems.map(item => item.content.getId()),
];

View File

@@ -139,6 +139,8 @@ type DropContainerProps = {|
// Computes drop areas and drop indicator indent.
windowSize: WindowSizeType,
// The Indent Scale used for the Events Sheet.
indentScale: number,
// Used only for the node just above dragged node if it is an only child,
// so that drop area covers the whole dragged node's row in height.
draggedNodeHeight: number,
@@ -225,6 +227,7 @@ export function DropContainer({
onDrop,
activateTargets,
windowSize,
indentScale,
draggedNodeHeight,
getNodeAtPath,
}: DropContainerProps) {
@@ -234,7 +237,7 @@ export function DropContainer({
// child of the event is the dragged one.
const canHaveSubEvents = !!node.event && node.event.canHaveSubEvents();
const indentWidth = getIndentWidth(windowSize);
const indentWidth = getIndentWidth(windowSize) * indentScale;
const dropAreaStyles = getTargetPositionStyles(
indentWidth,
draggedNodeHeight,

View File

@@ -11,7 +11,7 @@ import {
shouldActivate,
shouldValidate,
} from '../../UI/KeyboardShortcuts/InteractionKeys';
import LocalIcon from '../../UI/CustomSvgIcons/ExternalEvents';
import LocalVariableIcon from '../../UI/CustomSvgIcons/LocalVariable';
import { getVariableTypeIcon } from '../ParameterFields/VariableField';
const gd: libGDevelop = global.gd;
@@ -98,7 +98,7 @@ export const VariableDeclaration = (props: Props) => {
<Trans>
Declare{' '}
<span>
<LocalIcon
<LocalVariableIcon
className={classNames({
[icon]: true,
})}

View File

@@ -336,6 +336,7 @@ type EventsTreeProps = {|
windowSize: WindowSizeType,
eventsSheetHeight: number,
fontSize?: number,
indentScale: number,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
@@ -827,7 +828,11 @@ export default class ThemableEventsTree extends Component<
key={event.ptr}
eventsHeightsCache={this.eventsHeightsCache}
selection={this.props.selection}
leftIndentWidth={depth * getIndentWidth(this.props.windowSize)}
leftIndentWidth={
depth *
(getIndentWidth(this.props.windowSize) *
this.props.indentScale)
}
onAddNewInstruction={instructionsListContext =>
this.props.onAddNewInstruction(
eventContext,
@@ -929,6 +934,7 @@ export default class ThemableEventsTree extends Component<
onDrop={this._onDrop}
activateTargets={!isDragged && !!this.state.draggedNode}
windowSize={this.props.windowSize}
indentScale={this.props.indentScale}
getNodeAtPath={path =>
getNodeAtPath({
path,
@@ -1018,7 +1024,9 @@ export default class ThemableEventsTree extends Component<
)}
<SortableTree
treeData={treeData}
scaffoldBlockPxWidth={getIndentWidth(this.props.windowSize)}
scaffoldBlockPxWidth={
getIndentWidth(this.props.windowSize) * this.props.indentScale
}
onChange={noop}
onVisibilityToggle={this._onVisibilityToggle}
canDrag={false}

View File

@@ -277,6 +277,7 @@ const InstructionEditorDialog = ({
ref={instructionParametersEditor}
focusOnMount={shouldAutofocusInput && !!instructionType}
noHelpButton
id="object-instruction-parameters"
/>
);

View File

@@ -437,11 +437,15 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
</Subheader>
)}
{displayedObjectGroupsList.map(
({ item: groupWithContext, matches }) => {
({ item: groupWithContext, matches }, index) => {
const results = [];
results.push(
renderGroupObjectsListItem({
id: 'objectGroup-item-' + index,
data: {
objectName: groupWithContext.group.getName(),
},
groupWithContext,
iconSize,
onClick: () =>

View File

@@ -84,6 +84,7 @@ type Props = {|
type: string
) => void,
noHelpButton?: boolean,
id?: string,
|};
const isParameterVisible = (
@@ -119,6 +120,7 @@ const InstructionParametersEditor = React.forwardRef<
style,
openInstructionOrExpression,
resourceManagementProps,
id,
},
ref
) => {
@@ -258,7 +260,7 @@ const InstructionParametersEditor = React.forwardRef<
return (
<I18n>
{({ i18n }) => (
<ScrollView autoHideScrollbar>
<ScrollView autoHideScrollbar id={id}>
<Column expand>
<Line alignItems="flex-start">
<img

View File

@@ -8,6 +8,7 @@ import {
getObjectOrObjectGroupListItemValue,
} from './Keys';
import HighlightedText from '../../../UI/Search/HighlightedText';
import { type HTMLDataset } from '../../../Utils/HTMLDataset';
type Props = {|
groupWithContext: GroupWithContext,
@@ -15,6 +16,8 @@ type Props = {|
onClick: () => void,
selectedValue: ?string,
matchesCoordinates: number[][],
id: ?string,
data?: HTMLDataset,
|};
export const renderGroupObjectsListItem = ({
@@ -23,10 +26,14 @@ export const renderGroupObjectsListItem = ({
onClick,
selectedValue,
matchesCoordinates,
id,
data,
}: Props) => {
const groupName: string = groupWithContext.group.getName();
return (
<ListItem
id={id}
data={data}
key={getObjectGroupListItemKey(groupWithContext)}
selected={
selectedValue === getObjectOrObjectGroupListItemValue(groupName)

View File

@@ -5,6 +5,7 @@ import VariableField, {
getRootVariableName,
renderVariableWithIcon,
type VariableFieldInterface,
type VariableDialogOpeningProps,
} from './VariableField';
import GlobalAndSceneVariablesDialog from '../../VariablesList/GlobalAndSceneVariablesDialog';
import {
@@ -18,7 +19,10 @@ import { mapFor } from '../../Utils/MapFor';
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function AnyVariableField(props: ParameterFieldProps, ref) {
const field = React.useRef<?VariableFieldInterface>(null);
const [editorOpen, setEditorOpen] = React.useState(false);
const [
editorOpen,
setEditorOpen,
] = React.useState<VariableDialogOpeningProps | null>(null);
const focus: FieldFocusFunction = options => {
if (field.current) field.current.focus(options);
};
@@ -66,7 +70,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
if (selectedVariableName && selectedVariableName.startsWith(value)) {
onChange(selectedVariableName);
}
setEditorOpen(false);
setEditorOpen(null);
// The variable editor may have refactor the events for a variable type
// change which may have change the currently edited instruction type.
if (onInstructionTypeChanged) onInstructionTypeChanged();
@@ -97,7 +101,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
onRequestClose={props.onRequestClose}
onApply={props.onApply}
ref={field}
onOpenDialog={() => setEditorOpen(true)}
onOpenDialog={setEditorOpen}
globalObjectsContainer={props.globalObjectsContainer}
objectsContainer={props.objectsContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
@@ -113,10 +117,11 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
<GlobalAndSceneVariablesDialog
projectScopedContainersAccessor={projectScopedContainersAccessor}
open
onCancel={() => setEditorOpen(false)}
onCancel={() => setEditorOpen(null)}
onApply={onVariableEditorApply}
isGlobalTabInitiallyOpen={isGlobal}
initiallySelectedVariableName={props.value}
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
/>
)}
</React.Fragment>

View File

@@ -4,6 +4,7 @@ import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flo
import VariableField, {
renderVariableWithIcon,
type VariableFieldInterface,
type VariableDialogOpeningProps,
} from './VariableField';
import GlobalVariablesDialog from '../../VariablesList/GlobalVariablesDialog';
import {
@@ -12,12 +13,15 @@ import {
type FieldFocusFunction,
} from './ParameterFieldCommons';
import { enumerateVariables } from './EnumerateVariables';
import GlobalIcon from '../../UI/CustomSvgIcons/Publish';
import GlobalVariableIcon from '../../UI/CustomSvgIcons/GlobalVariable';
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function GlobalVariableField(props: ParameterFieldProps, ref) {
const field = React.useRef<?VariableFieldInterface>(null);
const [editorOpen, setEditorOpen] = React.useState(false);
const [
editorOpen,
setEditorOpen,
] = React.useState<VariableDialogOpeningProps | null>(null);
const focus: FieldFocusFunction = options => {
if (field.current) field.current.focus(options);
};
@@ -53,7 +57,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
onRequestClose={props.onRequestClose}
onApply={props.onApply}
ref={field}
onOpenDialog={() => setEditorOpen(true)}
onOpenDialog={setEditorOpen}
globalObjectsContainer={props.globalObjectsContainer}
objectsContainer={props.objectsContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
@@ -62,8 +66,8 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
{editorOpen && project && (
<GlobalVariablesDialog
project={project}
open={editorOpen}
onCancel={() => setEditorOpen(false)}
open
onCancel={() => setEditorOpen(null)}
onApply={(selectedVariableName: string | null) => {
if (
selectedVariableName &&
@@ -71,10 +75,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
) {
props.onChange(selectedVariableName);
}
setEditorOpen(false);
setEditorOpen(null);
if (field.current) field.current.updateAutocompletions();
}}
preventRefactoringToDeleteInstructions
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
/>
)}
</React.Fragment>
@@ -84,4 +90,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
export const renderInlineGlobalVariable = (
props: ParameterInlineRendererProps
) => renderVariableWithIcon(props, 'global variable', GlobalIcon);
) => renderVariableWithIcon(props, 'global variable', GlobalVariableIcon);

View File

@@ -33,7 +33,7 @@ const getRequiredBehaviorTypes = (
break;
}
const behaviorType = behaviorParameter.getExtraInfo();
if (behaviorType.length === '') {
if (behaviorType.length === 0) {
continue;
}
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(

View File

@@ -4,6 +4,7 @@ import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flo
import VariableField, {
renderVariableWithIcon,
type VariableFieldInterface,
type VariableDialogOpeningProps,
} from './VariableField';
import ObjectVariablesDialog from '../../VariablesList/ObjectVariablesDialog';
import {
@@ -14,7 +15,7 @@ import {
import { getLastObjectParameterValue } from './ParameterMetadataTools';
import getObjectByName from '../../Utils/GetObjectByName';
import getObjectGroupByName from '../../Utils/GetObjectGroupByName';
import ObjectIcon from '../../UI/CustomSvgIcons/Object';
import ObjectVariableIcon from '../../UI/CustomSvgIcons/ObjectVariable';
import { enumerateVariables } from './EnumerateVariables';
import { intersectionBy } from 'lodash';
@@ -59,7 +60,10 @@ export const getObjectOrGroupVariablesContainers = (
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function ObjectVariableField(props: ParameterFieldProps, ref) {
const field = React.useRef<?VariableFieldInterface>(null);
const [editorOpen, setEditorOpen] = React.useState(false);
const [
editorOpen,
setEditorOpen,
] = React.useState<VariableDialogOpeningProps | null>(null);
const focus: FieldFocusFunction = options => {
if (field.current) field.current.focus(options);
};
@@ -79,6 +83,8 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
expression,
parameterIndex,
onInstructionTypeChanged,
value,
onChange,
} = props;
const objectName = getLastObjectParameterValue({
@@ -112,6 +118,20 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
[variablesContainers]
);
const onVariableEditorApply = React.useCallback(
(selectedVariableName: string | null) => {
if (selectedVariableName && selectedVariableName.startsWith(value)) {
onChange(selectedVariableName);
}
setEditorOpen(null);
// The variable editor may have refactored the events for a variable type
// change which may have changed the currently edited instruction type.
if (onInstructionTypeChanged) onInstructionTypeChanged();
if (field.current) field.current.updateAutocompletions();
},
[onChange, onInstructionTypeChanged, value]
);
return (
<React.Fragment>
<VariableField
@@ -133,9 +153,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
onApply={props.onApply}
ref={field}
// There is no variable editor for groups.
onOpenDialog={
variablesContainers.length === 1 ? () => setEditorOpen(true) : null
}
onOpenDialog={variablesContainers.length === 1 ? setEditorOpen : null}
globalObjectsContainer={props.globalObjectsContainer}
objectsContainer={props.objectsContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
@@ -154,21 +172,12 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
projectScopedContainersAccessor={projectScopedContainersAccessor}
objectName={objectName}
variablesContainer={variablesContainers[0]}
open={editorOpen}
onCancel={() => setEditorOpen(false)}
onApply={(selectedVariableName: string | null) => {
if (
selectedVariableName &&
selectedVariableName.startsWith(props.value)
) {
props.onChange(selectedVariableName);
}
setEditorOpen(false);
if (onInstructionTypeChanged) onInstructionTypeChanged();
if (field.current) field.current.updateAutocompletions();
}}
open
onCancel={() => setEditorOpen(null)}
onApply={onVariableEditorApply}
preventRefactoringToDeleteInstructions
initiallySelectedVariableName={props.value}
initiallySelectedVariableName={editorOpen.variableName}
shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
/>
)}
</React.Fragment>
@@ -178,4 +187,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
export const renderInlineObjectVariable = (
props: ParameterInlineRendererProps
) => renderVariableWithIcon(props, 'object variable', ObjectIcon);
) => renderVariableWithIcon(props, 'object variable', ObjectVariableIcon);

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