Compare commits
79 Commits
use-multip
...
allow-bump
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d161d7b8f | ||
![]() |
98befc8000 | ||
![]() |
377231fb37 | ||
![]() |
22ae8ac489 | ||
![]() |
40674fd3b9 | ||
![]() |
0792e59b24 | ||
![]() |
8ba6ad7b43 | ||
![]() |
84b07bc84d | ||
![]() |
c46d39cbed | ||
![]() |
b96132964c | ||
![]() |
130cd1e3a7 | ||
![]() |
fbbcf25bf5 | ||
![]() |
ffaae4d3d4 | ||
![]() |
75d79a7758 | ||
![]() |
d91fb78848 | ||
![]() |
e0a2ed1654 | ||
![]() |
d5f2be1c19 | ||
![]() |
7abcfe8af2 | ||
![]() |
b86dd9efce | ||
![]() |
15f6b62c5b | ||
![]() |
eb995ec7c7 | ||
![]() |
a076571120 | ||
![]() |
ec9cb790e7 | ||
![]() |
4a283add00 | ||
![]() |
6b3faa42bb | ||
![]() |
555ee61e63 | ||
![]() |
b838c8549b | ||
![]() |
33db6ee359 | ||
![]() |
f361d3e1fa | ||
![]() |
441401f34c | ||
![]() |
558daa2075 | ||
![]() |
cd475316df | ||
![]() |
8b21e72c85 | ||
![]() |
a9d6f18c11 | ||
![]() |
f58e1113b6 | ||
![]() |
f82b5fc66d | ||
![]() |
2f19a9bb33 | ||
![]() |
8f739d85c2 | ||
![]() |
f23847617d | ||
![]() |
876332a782 | ||
![]() |
93c74c9fd6 | ||
![]() |
e92d8496ac | ||
![]() |
35e67a6d26 | ||
![]() |
740c7ae8bc | ||
![]() |
432a91c47b | ||
![]() |
799bc762aa | ||
![]() |
147a0eed53 | ||
![]() |
920e1d423f | ||
![]() |
c013f319ee | ||
![]() |
db53e84c6f | ||
![]() |
4b85f710a9 | ||
![]() |
07a350dadd | ||
![]() |
7ccbf91973 | ||
![]() |
410fecf715 | ||
![]() |
967bf5cbe3 | ||
![]() |
0020f72850 | ||
![]() |
0eb7b85e77 | ||
![]() |
063bf51783 | ||
![]() |
482fb3b85e | ||
![]() |
9a4a84d2af | ||
![]() |
9a705b98e2 | ||
![]() |
da940abdc2 | ||
![]() |
528b8f4e6c | ||
![]() |
7212d56a1b | ||
![]() |
5cba43335c | ||
![]() |
324698e269 | ||
![]() |
d219bf05d3 | ||
![]() |
9cbc421d74 | ||
![]() |
f63d9d1b5c | ||
![]() |
dd5d0669b1 | ||
![]() |
e681632e60 | ||
![]() |
0bcb219e00 | ||
![]() |
6d5e9c1676 | ||
![]() |
5ded5648bf | ||
![]() |
687f926bc5 | ||
![]() |
4a2d573956 | ||
![]() |
ce77414f85 | ||
![]() |
298ff6311d | ||
![]() |
ca0a000ae1 |
@@ -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:
|
||||
|
35
.travis.yml
@@ -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 ..
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -58,6 +58,7 @@ struct GD_CORE_API ExpressionParserError {
|
||||
MalformedObjectParameter,
|
||||
UnknownParameterType,
|
||||
MissingBehavior,
|
||||
VariableNameCollision,
|
||||
};
|
||||
|
||||
ExpressionParserError(gd::ExpressionParserError::ErrorType type_,
|
||||
|
@@ -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"),
|
||||
|
@@ -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");
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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) {
|
||||
|
273
Extensions/Multiplayer/multiplayerVariablesManager.ts
Normal 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();
|
||||
}
|
@@ -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',
|
||||
|
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
10
Extensions/Multiplayer/peer.js
Normal file
1
Extensions/Multiplayer/peer.js.map
Normal file
454
Extensions/Multiplayer/peerJsHelper.ts
Normal 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
@@ -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;
|
||||
}
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
) => {
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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.
|
@@ -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 {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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 ¶meters = eventsFunction.GetParameters();
|
||||
if (parameters.size() == 0) {
|
||||
return defaultSentence;
|
||||
}
|
||||
defaultSentence += " (";
|
||||
for (size_t parameterIndex = firstParameterIndex;
|
||||
parameterIndex < parameters.size(); parameterIndex++) {
|
||||
auto ¶meter = 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 ¶meter) {
|
||||
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
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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:*" />
|
||||
|
@@ -397,9 +397,7 @@ namespace gdjs {
|
||||
};
|
||||
}
|
||||
|
||||
updateFromObjectNetworkSyncData(
|
||||
networkSyncData: SpriteAnimatorNetworkSyncData
|
||||
) {
|
||||
updateFromNetworkSyncData(networkSyncData: SpriteAnimatorNetworkSyncData) {
|
||||
this._currentAnimation = networkSyncData.an;
|
||||
this._currentDirection = networkSyncData.di;
|
||||
this._currentFrameIndex = networkSyncData.fr;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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.`
|
||||
|
@@ -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 ||
|
||||
|
23
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
4888
GDJS/tests/games/multiplayer-variables-sync/game.json
Normal 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',
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
2
GDevelop.js/types.d.ts
vendored
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 |
@@ -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 |
@@ -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
|
||||
}
|
||||
]
|
@@ -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())}
|
||||
`,
|
||||
|
@@ -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}
|
||||
|
@@ -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(
|
||||
|
@@ -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()),
|
||||
];
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
})}
|
||||
|
@@ -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}
|
||||
|
@@ -277,6 +277,7 @@ const InstructionEditorDialog = ({
|
||||
ref={instructionParametersEditor}
|
||||
focusOnMount={shouldAutofocusInput && !!instructionType}
|
||||
noHelpButton
|
||||
id="object-instruction-parameters"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@@ -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: () =>
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -33,7 +33,7 @@ const getRequiredBehaviorTypes = (
|
||||
break;
|
||||
}
|
||||
const behaviorType = behaviorParameter.getExtraInfo();
|
||||
if (behaviorType.length === '') {
|
||||
if (behaviorType.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
|
@@ -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);
|
||||
|