Compare commits

...

50 Commits

Author SHA1 Message Date
Clément Pasteau
625fef0d39 Make Multiplayer extension independent from p2p extension + add message compression 2024-06-26 16:08:01 +02:00
Aurélien Vivet
558daa2075 Remove extra "Add" words in the events context menu (#6713) 2024-06-26 09:37:50 +02:00
AlexandreS
cd475316df Limit leaderboard customization css field length (#6720) 2024-06-26 07:42:41 +02:00
D8H
8b21e72c85 Fix to avoid new variables from being added at the top (#6718) 2024-06-25 17:34:12 +02:00
github-actions[bot]
a9d6f18c11 Update translations [skip ci] (#6689)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-06-25 17:10:32 +02:00
Clément Pasteau
f58e1113b6 Add new actions and expressions for multiplayer (#6717)
* Send a variable with a custom message
* Retrieve the player number of the sender
2024-06-25 17:07:38 +02:00
D8H
f82b5fc66d Fix local variables default values when the wait action is used (#6715) 2024-06-25 16:14:40 +02:00
AlexandreS
2f19a9bb33 Improve some icons in the editor (variables, mobile toolbar) (#6700) 2024-06-25 12:42:41 +02:00
Dennis Fehr
8f739d85c2 Fix dragging bug in the Events Sheet when the indent scale is not 1x (#6702) 2024-06-25 10:07:09 +02:00
AlexandreS
f23847617d Add possibility for players to login with Google/Apple in game. (#6711) 2024-06-24 18:22:43 +02:00
Clément Pasteau
876332a782 Fix multiplayer lobbies properly opening after login (#6712) 2024-06-24 18:15:32 +02:00
D8H
93c74c9fd6 Use property names instead of property labels in descriptions and sentences of internal behavior instructions (#6708) 2024-06-23 17:45:21 +02:00
Florian Rival
e92d8496ac Add missing Spine Runtime license agreement and link to Spine website (#6701) 2024-06-21 18:18:03 +02:00
AlexandreS
35e67a6d26 Try to register game before copying lobby configuration (#6696)
Don't show in changelog
2024-06-20 18:04:29 +02:00
Clément Pasteau
740c7ae8bc Upgrade CircleCI macOS builders to m1 (#6688)
Only show in developer changelog
2024-06-20 10:26:47 +02:00
Clément Pasteau
432a91c47b Bump to 5.4.204 (#6682) 2024-06-20 08:40:39 +02:00
Clément Pasteau
799bc762aa Fix wrong import (#6687)
Do not show in changelog
2024-06-20 08:40:26 +02:00
github-actions[bot]
147a0eed53 Update translations [skip ci] (#6683)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-20 07:58:09 +02:00
Aurélien Vivet
920e1d423f rename tutorial (#6684)
Do not show in changelog
2024-06-19 20:41:05 +02:00
Aurélien Vivet
c013f319ee Add the new guided lesson: Fire Bullets with an action from a behavior (#6681) 2024-06-19 19:49:46 +02:00
Clément Pasteau
db53e84c6f Add more params to lobby connection (#6677)
Do not show in changelog
2024-06-19 19:02:13 +02:00
github-actions[bot]
4b85f710a9 Update translations [skip ci] (#6673)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-19 18:06:54 +02:00
Clément Pasteau
07a350dadd Use a separate logger for debugging multiplayer (#6680)
Do not show in changelog
2024-06-19 17:07:20 +02:00
Clément Pasteau
7ccbf91973 Automatically disable default controls for synchronized objects (#6678)
* For top down & platformer objects, when the object is not owned by the current player, it will automatically have default controls disabled
2024-06-18 18:19:56 +02:00
Clément Pasteau
410fecf715 Multiplayer improvements (#6676)
* An object changing layer during the game is now properly moved to the layer on the other players games
* Video object can be synced as well
* Pathfinding behavior is now properly synced, allowing a smooth prediction on other players games
* Spine object is now synced as well
2024-06-18 16:52:41 +02:00
AlexandreS
967bf5cbe3 Reinstate shadow behind snackbar (#6674)
Don't show in changelog
2024-06-18 15:37:32 +02:00
github-actions[bot]
0020f72850 Update translations [skip ci] (#6669)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-18 12:39:01 +02:00
Clément Pasteau
0eb7b85e77 Improve multiplayer sync (#6672)
* Fix a rare case where the lobby was joined but did not appear as such in the interface
* Text inputs are now synced properly
* Add 2 new expressions for current player ping & username
* Fix Physics2 behavior being properly synced
2024-06-18 12:38:16 +02:00
Florian Rival
063bf51783 Add 'reference' to generated documentation reference page titles [skip ci]
Don't show in changelog
2024-06-18 10:50:50 +02:00
Clément Pasteau
482fb3b85e Allow customizing the Events sheet indent scale (#6671)
Co-authored-by: Dennis Fehr <dennis.james.fehr@gmail.com> @cyraid
2024-06-18 10:49:39 +02:00
Florian Rival
9a4a84d2af Fix Player object ownership action/condition sentence 2024-06-17 21:56:50 +02:00
Florian Rival
9a705b98e2 Fix crash when launching multiple previews with a pop-up blocker 2024-06-17 18:37:50 +02:00
Clément Pasteau
da940abdc2 Multiplayer fixes (#6668)
* Prevent verticesBuffer to be synced for Physics2
* Ensure we create a force if no recycled are available
2024-06-17 18:35:20 +02:00
github-actions[bot]
528b8f4e6c Update translations [skip ci] (#6663)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-17 15:06:36 +02:00
Clément Pasteau
7212d56a1b Add folders for multiplayer extension (#6667)
Do not show in changelog
2024-06-17 14:53:12 +02:00
Clément Pasteau
5cba43335c Fix sentence for taking ownership (#6666)
Do not show in changelog
2024-06-17 14:28:08 +02:00
Clément Pasteau
324698e269 Fix parsing wrongly the variableNetworkId (#6665)
Do not show in changelog
2024-06-17 14:19:40 +02:00
Clément Pasteau
d219bf05d3 Fix the animation frame update for a Sprite when receiving new animation from the network (#6664) 2024-06-17 13:20:26 +02:00
D8H
9cbc421d74 Generate a default sentence for event-based functions (#6638) 2024-06-16 23:18:54 +02:00
Florian Rival
f63d9d1b5c Bump version 2024-06-16 10:30:32 +02:00
github-actions[bot]
dd5d0669b1 Update translations [skip ci] (#6661)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2024-06-16 10:29:40 +02:00
Clément Pasteau
e681632e60 Fix correctly parsing custom messages (#6662)
Do not show in changelog
2024-06-14 18:45:16 +02:00
AlexandreS
0bcb219e00 Inform lobbies iframe if opened from preview in native mobile app (#6659)
Don't show in changelog
2024-06-14 17:09:55 +02:00
D8H
6d5e9c1676 Fix regression on activate behavior action parameter (typo) (#6660)
- Don't show in changelog
2024-06-14 17:06:26 +02:00
github-actions[bot]
5ded5648bf Update translations [skip ci] (#6658)
Co-authored-by: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com>
2024-06-14 16:39:48 +02:00
AlexandreS
687f926bc5 Add logic to copy lobby configuration when opening an example (#6657)
Don't show in changelog
2024-06-14 15:41:58 +02:00
github-actions[bot]
4a2d573956 Update translations [skip ci] (#6641)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2024-06-14 15:39:12 +02:00
Clément Pasteau
ce77414f85 Allow assigning variable owners for multiplayer games (#6651)
* Actions can be used at the beginning of the game or a scene, to define scene and global variables as synchronised by another player, or not synchronised at all
2024-06-14 15:37:37 +02:00
D8H
298ff6311d Fix game properties editor to avoid empty values to make project saving fail (#6656) 2024-06-14 13:04:47 +02:00
Clément Pasteau
ca0a000ae1 Use a unique key for root extension configuration row (#6655)
Do not show in changelog
2024-06-14 11:21:58 +02:00
180 changed files with 11728 additions and 1742 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,15 +39,15 @@ module.exports = {
'Open the game lobbies window, where players can join lobbies or see the one they are in.'
),
_('Open the game lobbies'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.addCodeOnlyParameter('currentScene', '')
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -56,6 +56,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.openLobbiesWindow');
@@ -65,7 +66,7 @@ module.exports = {
_('Allow players to close the lobbies window'),
_('Allow players to close the lobbies window. Allowed by default.'),
_('Allow players to close the lobbies window: _PARAM1_'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -73,8 +74,8 @@ module.exports = {
.addParameter('yesorno', _('Show close button'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -83,6 +84,7 @@ module.exports = {
)
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
.setFunctionName('gdjs.multiplayer.showLobbiesCloseButton');
@@ -94,14 +96,14 @@ module.exports = {
'End the lobby game. This will trigger the "Lobby game has just ended" condition.'
),
_('End the lobby game'),
'',
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -110,6 +112,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.endLobbyGame');
@@ -121,7 +124,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 +132,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 +142,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 +216,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 +232,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 +242,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 +257,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 +267,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 +282,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 +292,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 +307,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 +319,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 +335,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 +347,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 +362,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 +372,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 +387,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 +397,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 +413,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 +424,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 +439,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
.addStrExpression(
'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 +487,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 +512,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 +532,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 +542,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 +584,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 +594,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 +837,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 +846,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 +858,8 @@ module.exports = {
'PlayerObjectOwnership',
_('Player object ownership'),
_('the player owning the object'),
_('the player owning the object'),
'',
_('the player owning the instance'),
_('Objects'),
'JsPlatform/Extensions/multiplayer.svg'
)
.addParameter('object', _('Object'), '', false)
@@ -600,7 +880,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'),
'',
_('Objects'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -622,7 +902,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_'),
'',
_('Objects'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)
@@ -644,7 +924,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_'),
'',
_('Objects'),
'JsPlatform/Extensions/multiplayer.svg',
'JsPlatform/Extensions/multiplayer.svg'
)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,15 +1,12 @@
namespace gdjs {
declare var cordova: any;
const logger = new gdjs.Logger('Multiplayer');
const multiplayerComponents = gdjs.multiplayerComponents;
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;
@@ -46,16 +43,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 +72,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();
@@ -104,6 +117,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 +162,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 +181,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 +226,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 +249,7 @@ namespace gdjs {
);
if (playerLeftPublicProfile) {
multiplayerComponents.displayPlayerLeftNotification(
gdjs.multiplayerComponents.displayPlayerLeftNotification(
runtimeScene,
(playerLeftPublicProfile && playerLeftPublicProfile.username) ||
'Player'
@@ -371,7 +395,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 +404,20 @@ 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 plarformInfo = runtimeScene.getGame().getPlatformInfo();
_websocket.send(
JSON.stringify({
action: 'sessionInformation',
connectionType: 'lobby',
isCordova: plarformInfo.isCordova,
devicePlatform: plarformInfo.devicePlatform,
navigatorPlatform: plarformInfo.navigatorPlatform,
hasTouch: plarformInfo.hasTouch,
})
);
}
};
_websocket.onmessage = (event) => {
@@ -399,6 +433,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;
}
@@ -473,7 +512,7 @@ namespace gdjs {
return;
}
const lobbiesIframe = multiplayerComponents.getLobbiesIframe(
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
runtimeScene
);
@@ -523,7 +562,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 +570,7 @@ namespace gdjs {
}
}
if (brokerServerConfig) {
gdjs.evtTools.p2p.useCustomBrokerServer(
gdjs.multiplayerPeerJsHelper.useCustomBrokerServer(
brokerServerConfig.hostname,
brokerServerConfig.port,
brokerServerConfig.path,
@@ -539,7 +578,7 @@ namespace gdjs {
brokerServerConfig.secure
);
} else {
gdjs.evtTools.p2p.useDefaultBrokerServer();
gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();
}
_connectionId = connectionId;
@@ -548,7 +587,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
);
@@ -606,7 +645,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
);
@@ -628,12 +667,12 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene
) {
// 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
);
@@ -650,7 +689,9 @@ namespace gdjs {
);
// Prevent the player from leaving the lobby while the game is starting.
multiplayerComponents.hideLobbiesCloseButtonTemporarily(runtimeScene);
gdjs.multiplayerComponents.hideLobbiesCloseButtonTemporarily(
runtimeScene
);
};
/**
@@ -664,9 +705,11 @@ namespace gdjs {
// 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 +775,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();
@@ -744,7 +787,7 @@ namespace gdjs {
*/
const handlePeerIdEvent = function (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 +800,7 @@ namespace gdjs {
return;
}
gdjs.evtTools.p2p.connect(peerId);
gdjs.multiplayerPeerJsHelper.connect(peerId);
};
/**
@@ -819,6 +862,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 +914,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 +954,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 +993,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 +1038,7 @@ namespace gdjs {
};
window.addEventListener('message', _lobbiesMessageCallback, true);
multiplayerComponents.displayIframeInsideLobbiesContainer(
gdjs.multiplayerComponents.displayIframeInsideLobbiesContainer(
runtimeScene,
targetUrl
);
@@ -994,7 +1066,7 @@ namespace gdjs {
return;
}
if (_isCheckingIfGameIsRegistered || _isWaitingForLoginCallback) {
if (_isCheckingIfGameIsRegistered || _isWaitingForLogin) {
// The action is called multiple times, let's prevent that.
return;
}
@@ -1019,13 +1091,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 +1106,7 @@ namespace gdjs {
return;
}
multiplayerComponents.displayLobbies(
gdjs.multiplayerComponents.displayLobbies(
runtimeScene,
onLobbiesContainerDismissed
);
@@ -1076,7 +1148,7 @@ namespace gdjs {
'_blank'
);
multiplayerComponents.addTextsToLoadingContainer(
gdjs.multiplayerComponents.addTextsToLoadingContainer(
runtimeScene,
_isGameRegistered,
wikiOpenAction
@@ -1093,7 +1165,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 +1175,7 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene,
visible: boolean
) {
multiplayerComponents.changeLobbiesWindowCloseActionVisibility(
gdjs.multiplayerComponents.changeLobbiesWindowCloseActionVisibility(
runtimeScene,
visible
);
@@ -1116,7 +1188,7 @@ namespace gdjs {
runtimeScene: gdjs.RuntimeScene
) {
removeLobbiesCallbacks();
multiplayerComponents.removeLobbiesContainer(runtimeScene);
gdjs.multiplayerComponents.removeLobbiesContainer(runtimeScene);
};
/*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,414 @@
/// <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;
};
/**
* 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[] = [];
/**
* Helper function to compress data sent over the network.
*/
async function compressData(data: object): Promise<Uint8Array> {
const jsonString = JSON.stringify(data); // Convert object to JSON string
const encoder = new TextEncoder();
const array = encoder.encode(jsonString); // Convert string to Uint8Array
// @ts-ignore - As of 09/2023 the CompressionStream is now available in all browsers.
const cs = new CompressionStream('gzip'); // Create a CompressionStream with gzip
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(
compressedData: Uint8Array
): Promise<object | undefined> {
// @ts-ignore - As of 09/2023 the DecompressionStream is now available in all browsers.
const ds = new DecompressionStream('gzip'); // Create a DecompressionStream with gzip
const writer = ds.writable.getWriter();
writer.write(compressedData);
writer.close();
const decompressedStream = ds.readable;
const reader = decompressedStream.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const decompressedData = new Uint8Array(
chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])
);
const decoder = new TextDecoder();
const jsonStringData = decoder.decode(decompressedData); // Convert Uint8Array back to string
try {
const parsedData = JSON.parse(jsonStringData);
return parsedData;
} catch (e) {
logger.error(`Error while parsing message: ${e.toString()}`);
return;
}
}
/**
* Helper function to get the messages list for a given message name.
*/
export const getOrCreateMessagesList = (
messageName: string
): IMessagesList => {
const messagesList = allMessages.get(messageName);
if (messagesList) return messagesList;
const newMessagesList = new MessagesList(messageName);
allMessages.set(messageName, newMessagesList);
return newMessagesList;
};
/**
* Internal function called when a connection with a remote peer is initiated.
* @param connection The DataConnection of the peer
*/
const _onConnect = (connection: Peer.DataConnection<NetworkMessage>) => {
connections.set(connection.peer, connection);
connection.on('data', async (data) => {
if (isValidNetworkMessage(data)) {
const messagesList = getOrCreateMessagesList(data.messageName);
const messageSender = connection.peer;
const decompressedData = await decompressData(data.data);
if (!decompressedData) return;
messagesList.pushMessage(decompressedData, messageSender);
}
});
// Close event is only for graceful disconnection,
// but we want onDisconnect to trigger for any type of disconnection,
// so we register a listener for both event types.
connection.on('error', () => {
_onDisconnect(connection.peer);
});
connection.on('close', () => {
_onDisconnect(connection.peer);
});
// Regularly check for disconnection as the built in way is not reliable.
(function disconnectChecker() {
if (
connection.peerConnection &&
(connection.peerConnection.connectionState === 'failed' ||
connection.peerConnection.connectionState === 'disconnected' ||
connection.peerConnection.connectionState === 'closed')
) {
_onDisconnect(connection.peer);
} else {
setTimeout(disconnectChecker, 1000);
}
})();
};
/**
* Internal function called when a remote client disconnects.
* @param connectionID The ID of the peer that disconnected.
*/
const _onDisconnect = (connectionID: string) => {
if (!connections.has(connectionID)) return;
justDisconnectedPeers.push(connectionID);
connections.delete(connectionID);
};
/**
* Internal function called to initialize PeerJS after it
* has been configured.
*/
const loadPeerJS = () => {
if (peer !== null) return;
peer = new Peer(peerConfig);
peer.on('open', () => {
ready = true;
});
peer.on('error', (errorMessage) => {
logger.error('PeerJS error:', errorMessage);
});
peer.on('connection', (connection) => {
connection.on('open', () => {
_onConnect(connection);
justConnectedPeers.push(connection.peer);
});
});
peer.on('close', () => {
peer = null;
loadPeerJS();
});
peer.on('disconnected', peer.reconnect);
};
/**
* Connects to another p2p client.
* @param id - The other client's ID.
*/
export const connect = (id: string) => {
if (peer === null) return;
const connection = peer.connect(id);
connection.on('open', () => {
_onConnect(connection);
});
};
/**
* Disconnects from all other p2p clients.
*/
export const disconnectFromAllPeers = () => {
for (const connection of connections.values()) connection.close();
};
/**
* Send a message to a specific peer.
* @param ids - The IDs of the clients to send the event to.
* @param messageName - The event to trigger.
* @param eventData - Additional data to send with the event.
*/
export const sendDataTo = async (
ids: string[],
messageName: string,
messageData: object
) => {
if (!ids.length) return;
const compressedData = await compressData(messageData);
for (const id of ids) {
const connection = connections.get(id);
if (connection) {
connection.send({
messageName,
data: compressedData,
});
}
}
};
export const getAllMessagesMap = () => allMessages;
/**
* Connects to a custom broker server.
* @param host The host of the broker server.
* @param port The port of the broker server.
* @param path The path (part of the url after the host) to the broker server.
* @param key Optional password to connect to the broker server.
* @param ssl Use ssl?
*/
export const useCustomBrokerServer = (
host: string,
port: number,
path: string,
key: string,
ssl: boolean
) => {
Object.assign(peerConfig, {
host,
port,
path,
secure: ssl,
// All servers have "peerjs" as default key
key: key.length === 0 ? 'peerjs' : key,
});
loadPeerJS();
};
export const useDefaultBrokerServer = loadPeerJS;
/**
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
* @param urls The URL of the STUN/TURN server.
* @param username An optional username to send to the server.
* @param credential An optional password to send to the server.
*/
export const useCustomICECandidate = (
urls: string,
username?: string,
credential?: string
) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
peerConfig.config.iceServers.push({
urls,
username,
credential,
});
};
/**
* Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.
* @param shouldUseRelayServer Whether relay-only should be enabled or disabled.
*/
export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceTransportPolicy = shouldUseRelayServer
? 'relay'
: 'all';
};
/**
* Returns the own current peer ID.
* @see Peer.id
*/
export const getCurrentId = (): string => {
if (peer == undefined) return '';
return peer.id || '';
};
/**
* Returns true once PeerJS finished initialization.
* @see ready
*/
export const isReady = () => ready;
/**
* Return any disconnected peers.
*/
export const getJustDisconnectedPeers = () => justDisconnectedPeers;
/**
* Returns the list of all currently connected peers.
*/
export const getAllPeers = () => Array.from(connections.keys());
gdjs.callbacksRuntimeScenePostEvents.push(() => {
// Clear the list of messages at the end of the frame, assuming they've been all processed.
for (const messagesList of allMessages.values()) {
messagesList.getMessages().length = 0;
}
// Clear the list of just connected and disconnected peers.
if (justDisconnectedPeers.length > 0) {
justDisconnectedPeers.length = 0;
}
if (justConnectedPeers.length > 0) {
justConnectedPeers.length = 0;
}
});
}
}

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

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

View File

@@ -91,12 +91,12 @@ describe('Multiplayer', () => {
};
/**
* A mocked P2P event data.
* @implements {gdjs.evtTools.p2p.IEventData}
* A mocked P2P message data.
* @implements {gdjs.multiplayerPeerJsHelper.IMessageData}
*/
class MockedEventData {
class MockedMessageData {
/**
* @param {string} data
* @param {object} data
* @param {string} sender
**/
constructor(data, sender) {
@@ -107,145 +107,132 @@ describe('Multiplayer', () => {
/**
* The data sent alongside the event.
*/
data = '';
data = {};
/**
* The ID of the sender of the event.
*/
sender = '';
}
/**
* A mocked P2P event.
* @implements {gdjs.evtTools.p2p.IEvent}
*/
class MockedEvent {
data = [];
dataloss = false;
isTriggered() {
return this.data.length > 0;
}
/**
* @param {gdjs.evtTools.p2p.IEventData} newData
*/
pushData(newData) {
if (this.dataloss && this.data.length > 0) this.data[0] = newData;
else this.data.push(newData);
}
popData() {
this.data.shift();
}
getData() {
return this.data.length === 0 ? '' : this.data[0].data;
return this.data;
}
getSender() {
return this.data.length === 0 ? '' : this.data[0].sender;
return this.sender;
}
}
/**
* A mocked P2P messages list.
* @implements {gdjs.multiplayerPeerJsHelper.IMessagesList}
*/
class MockedMessagesList {
data = [];
messageName = 'some-message-name';
constructor(messageName) {
this.messageName = messageName;
}
getName() {
return this.messageName;
}
/**
* @param {object} newData
* @param {string} sender
*/
pushMessage(newData, sender) {
this.data.push(new MockedMessageData(newData, sender));
}
getMessages() {
return this.data;
}
}
/**
* Create a mocked P2P handler.
* It stores the events sent to/from peers.
* It stores the messages sent to/from peers.
*/
const createP2PAndMultiplayerMessageManagerMock = () => {
const createMultiplayerManagersMock = () => {
const p2pState = {
currentPeerId: '',
otherPeerIds: [],
};
/** @type {Record<string, Map<string, MockedEvent>>} */
const peerEvents = {};
/** @type {Record<string, Map<string, MockedMessagesList>>} */
const peerAllMessagesMap = {};
/** @type {Record<string, gdjs.MultiplayerMessageManager>} */
const peerMultiplayerMessageManager = {};
const getPeerEvents = (peerId) =>
(peerEvents[peerId] = peerEvents[peerId] || new Map());
/** @type {Record<string, gdjs.MultiplayerVariablesManager>} */
const peerMultiplayerVariablesManager = {};
const getPeerMessages = (peerId) =>
(peerAllMessagesMap[peerId] = peerAllMessagesMap[peerId] || new Map());
/**
* @param {string} eventName
* @returns {gdjs.evtTools.p2p.IEvent}
* @param {string} messageName
* @returns {gdjs.multiplayerPeerJsHelper.IMessagesList}
*/
const getEvent = (eventName) => {
const events = getPeerEvents(p2pState.currentPeerId);
let event = events.get(eventName);
if (!event) events.set(eventName, (event = new MockedEvent()));
return event;
const getOrCreateMessagesList = (messageName) => {
const allMessagesMap = getPeerMessages(p2pState.currentPeerId);
const messagesList = allMessagesMap.get(messageName);
if (messagesList) return messagesList;
const newMessagesList = new MockedMessagesList(messageName);
allMessagesMap.set(messageName, newMessagesList);
return newMessagesList;
};
/**
* @param {string} peerId
* @param {string} eventName
* @param {string} eventData
* @param {string[]} peerIds
* @param {string} messageName
* @param {object} messageData
*/
const sendDataTo = (peerId, eventName, eventData) => {
// console.log(`## SENDING DATA TO ${peerId}:`, eventName, eventData);
const events = getPeerEvents(peerId);
let event = events.get(eventName);
if (!event) events.set(eventName, (event = new MockedEvent()));
event.pushData(new MockedEventData(eventData, peerId));
const sendDataTo = async (peerIds, messageName, messageData) => {
for (const peerId of peerIds) {
// console.log(`## SENDING DATA TO ${peerId}:`, messageName, messageData);
const peerAllMessagesMap = getPeerMessages(peerId);
let peerMessagesList = peerAllMessagesMap.get(messageName);
if (!peerMessagesList) {
peerMessagesList = new MockedMessagesList(messageName);
peerAllMessagesMap.set(messageName, peerMessagesList);
}
peerMessagesList.pushMessage(messageData, p2pState.currentPeerId);
}
};
/** @type {typeof gdjs.evtTools.p2p} */
const p2pMock = {
/** @type {typeof gdjs.multiplayerPeerJsHelper} */
const peerJsHelperMock = {
// @ts-ignore - this is a mock so private properties can't be the same.
Event: MockedEvent,
EventData: MockedEventData,
sendVariableTo: () => {},
sendVariableToAll: () => {},
getEventVariable: (eventName, variable) => {
variable.fromJSON(getEvent(eventName).getData());
},
onEvent: (eventName, dataloss) => {
const event = getEvent(eventName);
event.dataloss = dataloss;
const isTriggered = event.isTriggered();
return isTriggered;
},
getEvent,
MessagesList: MockedMessagesList,
MessageData: MockedMessageData,
getOrCreateMessagesList,
connect: (id) => {},
disconnectFromPeer: (id) => {},
disconnectFromAllPeers: () => {},
disconnectFromAll: () => {},
disconnectFromBroker: () => {},
sendDataTo,
sendDataToAll: (eventName, eventData) => {
p2pState.otherPeerIds.forEach((peerId) => {
sendDataTo(peerId, eventName, eventData);
});
},
getEventData: (eventName) => getEvent(eventName).getData(),
getEventSender: (eventName) => getEvent(eventName).getSender(),
getEvents: () => getPeerEvents(p2pState.currentPeerId),
getAllMessagesMap: () => getPeerMessages(p2pState.currentPeerId),
useCustomBrokerServer: () => {},
useDefaultBrokerServer: () => {},
useCustomICECandidate: () => {},
forceUseRelayServer: (shouldUseRelayServer) => {},
overrideId: (id) => {},
getCurrentId: () => 'fake-current-id',
isReady: () => true,
onError: () => false,
getLastError: () => '',
onDisconnect: () => false,
getDisconnectedPeer: () => '',
onConnection: () => false,
getConnectedPeer: () => '',
getJustDisconnectedPeers: () => [],
getAllPeers: () => p2pState.otherPeerIds,
getConnectionInstance: () => undefined,
};
gdjs.evtTools.p2p = p2pMock;
gdjs.multiplayerPeerJsHelper = peerJsHelperMock;
return {
switchToPeer: ({ peerId, otherPeerIds, playerNumber }) => {
// console.log('## SWITCHING TO PEER', peerId);
// Switch the state of the P2P mock.
// Switch the state of the peerJs mock.
p2pState.currentPeerId = peerId;
p2pState.otherPeerIds = otherPeerIds;
@@ -253,6 +240,12 @@ describe('Multiplayer', () => {
gdjs.multiplayerMessageManager = peerMultiplayerMessageManager[peerId] =
peerMultiplayerMessageManager[peerId] ||
gdjs.makeMultiplayerMessageManager();
// Switch the state of the MultiplayerVariablesManager.
gdjs.multiplayerVariablesManager = peerMultiplayerVariablesManager[
peerId
] =
peerMultiplayerVariablesManager[peerId] ||
gdjs.makeMultiplayerVariablesManager();
// Ensure the messageManager is aware of the other players.
gdjs.multiplayerMessageManager.updatePlayersPingsForTests({
@@ -264,25 +257,29 @@ describe('Multiplayer', () => {
// Switch the state of the game.
gdjs.multiplayer.playerNumber = playerNumber;
},
logEvents: () => {
Object.keys(peerEvents).forEach((peerId) => {
console.log(`## PEER ${peerId} events:`);
for (const [eventName, event] of peerEvents[peerId]) {
console.log(`${eventName}: ${JSON.stringify(event.data)}`);
logMessages: () => {
Object.keys(peerAllMessagesMap).forEach((peerId) => {
console.log(`## PEER ${peerId} messages:`);
for (const [messageName, messagesList] of peerAllMessagesMap[
peerId
]) {
console.log(
`${messageName}: ${JSON.stringify(messagesList.getMessages())}`
);
}
});
},
markAllPeerEventsAsProcessed: () => {
for (const events of Object.values(peerEvents)) {
for (const event of events.values()) {
event.popData();
markAllPeerMessagesAsProcessed: () => {
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
for (const messagesList of allMessagesList.values()) {
messagesList.data = [];
}
}
},
expectNoEventsToBeProcessed: () => {
for (const events of Object.values(peerEvents)) {
for (const event of events.values()) {
expect(event.isTriggered()).to.be(false);
expectNoMessagesToBeProcessed: () => {
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
for (const messagesList of allMessagesList.values()) {
expect(messagesList.getMessages().length).to.be(0);
}
}
},
@@ -328,8 +325,8 @@ describe('Multiplayer', () => {
it('synchronizes scene/global variables from the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -345,9 +342,11 @@ describe('Multiplayer', () => {
const p1BooleanVariable = new gdjs.Variable();
p1BooleanVariable.setBoolean(false);
p1RuntimeScene.getVariables().add('MyStringVariable', p1StringVariable);
p1RuntimeScene.getVariables().add('MyNumberVariable', p1NumberVariable);
p1RuntimeScene.getVariables().add('MyBooleanVariable', p1BooleanVariable);
p1RuntimeScene.getVariables().add('MyString_Variable', p1StringVariable);
p1RuntimeScene.getVariables().add('MyNumber_Variable', p1NumberVariable);
p1RuntimeScene
.getVariables()
.add('MyBoolean_Variable', p1BooleanVariable);
p1RuntimeScene.renderAndStep(1000 / 60);
@@ -359,20 +358,24 @@ describe('Multiplayer', () => {
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
expect(p2RuntimeScene.getVariables().has('MyStringVariable')).to.be(true);
expect(p2RuntimeScene.getVariables().has('MyNumberVariable')).to.be(true);
expect(p2RuntimeScene.getVariables().has('MyBooleanVariable')).to.be(
markAllPeerMessagesAsProcessed();
expect(p2RuntimeScene.getVariables().has('MyString_Variable')).to.be(
true
);
expect(p2RuntimeScene.getVariables().has('MyNumber_Variable')).to.be(
true
);
expect(p2RuntimeScene.getVariables().has('MyBoolean_Variable')).to.be(
true
);
expect(
p2RuntimeScene.getVariables().get('MyStringVariable').getAsString()
p2RuntimeScene.getVariables().get('MyString_Variable').getAsString()
).to.be('Hello from remote world');
expect(
p2RuntimeScene.getVariables().get('MyNumberVariable').getAsNumber()
p2RuntimeScene.getVariables().get('MyNumber_Variable').getAsNumber()
).to.be(42);
expect(
p2RuntimeScene.getVariables().get('MyBooleanVariable').getAsBoolean()
p2RuntimeScene.getVariables().get('MyBoolean_Variable').getAsBoolean()
).to.be(false);
// Also check global variables.
@@ -444,7 +447,7 @@ describe('Multiplayer', () => {
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
expect(
p2RuntimeScene.getGame().getVariables().has('MyGlobalStringVariable')
).to.be(true);
@@ -514,9 +517,9 @@ describe('Multiplayer', () => {
it('overrides a scene/global variable, modified by a player, when synchronized by the host', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -551,7 +554,7 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().add('MyOtherVariable', variable);
}
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
expect(
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Hello from remote world');
@@ -559,7 +562,7 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
).to.be('Something else');
expectNoEventsToBeProcessed();
expectNoMessagesToBeProcessed();
// Check the host sends again the variable, even if not changed, for reliability
// (allows to work around a dropped message, without using a real acknowledgement).
@@ -590,15 +593,348 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
).to.be('Something else');
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
});
it('synchronizes a scene/global variable from a player to the host to other players', () => {
const {
switchToPeer,
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
otherPeerIds: ['player-2', 'player-3'],
playerNumber: 1,
});
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p1SceneVariable = new gdjs.Variable();
p1SceneVariable.setString('This will be overriden');
p1RuntimeScene.getVariables().add('MyVariable', p1SceneVariable);
p1SceneVariable.setPlayerOwnership(3); // Ownership is given to player 3.
const p1GlobalVariable = new gdjs.Variable();
p1GlobalVariable.setString('This will be overriden');
p1RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p1GlobalVariable);
p1GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
p1RuntimeScene.renderAndStep(1000 / 60);
switchToPeer({
peerId: 'player-2',
otherPeerIds: ['player-1'],
playerNumber: 2,
});
// Create the variable on player 2 too.
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p2Scenevariable = new gdjs.Variable();
p2Scenevariable.setString('This will be overriden');
p2RuntimeScene.getVariables().add('MyVariable', p2Scenevariable);
p2Scenevariable.setPlayerOwnership(3); // Ownership is given to player 3.
const p2GlobalVariable = new gdjs.Variable();
p2GlobalVariable.setString('This will be overriden');
p2RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p2GlobalVariable);
p2GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
p2RuntimeScene.renderAndStep(1000 / 60);
switchToPeer({
peerId: 'player-3',
otherPeerIds: ['player-1'],
playerNumber: 3,
});
// Create the variable on player 3 too, which is the owner.
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p3SceneVariable = new gdjs.Variable();
p3SceneVariable.setString('This will be overriden');
p3RuntimeScene.getVariables().add('MyVariable', p3SceneVariable);
p3SceneVariable.setPlayerOwnership(3); // Ownership is given to player 3.
const p3GlobalVariable = new gdjs.Variable();
p3GlobalVariable.setString('This will be overriden');
p3RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p3GlobalVariable);
p3GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Change the variables on player 3.
{
switchToPeer({
peerId: 'player-3',
otherPeerIds: ['player-1'],
playerNumber: 3,
});
p3RuntimeScene
.getVariables()
.get('MyVariable')
.setString('Changed by player 3');
p3RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.setString('Changed by player 3');
p3RuntimeScene.renderAndStep(1000 / 60);
}
// Check the variables are synchronized on the host.
{
switchToPeer({
peerId: 'player-1',
otherPeerIds: ['player-2', 'player-3'],
playerNumber: 1,
});
p1RuntimeScene.renderAndStep(1000 / 60);
expect(
p1RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Changed by player 3');
expect(
p1RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.getAsString()
).to.be('Changed by player 3');
}
// Check the variable is synchronized on the other player.
{
switchToPeer({
peerId: 'player-2',
otherPeerIds: ['player-1'],
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
expect(
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Changed by player 3');
expect(
p2RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.getAsString()
).to.be('Changed by player 3');
}
});
it('does not synchronize a scene/global variable from players if defined as not synchronized', () => {
const {
switchToPeer,
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
otherPeerIds: ['player-2', 'player-3'],
playerNumber: 1,
});
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p1SceneVariable = new gdjs.Variable();
p1SceneVariable.setString('P1 is master of this variable');
p1RuntimeScene.getVariables().add('MyVariable', p1SceneVariable);
p1SceneVariable.disableSynchronization(); // Disable synchronization.
const p1GlobalVariable = new gdjs.Variable();
p1GlobalVariable.setString('P1 is master of this variable');
p1RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p1GlobalVariable);
p1GlobalVariable.disableSynchronization(); // Disable synchronization.
p1RuntimeScene.renderAndStep(1000 / 60);
switchToPeer({
peerId: 'player-2',
otherPeerIds: ['player-1'],
playerNumber: 2,
});
// Create the variable on player 2 too.
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p2Scenevariable = new gdjs.Variable();
p2Scenevariable.setString('P2 is master of this variable');
p2RuntimeScene.getVariables().add('MyVariable', p2Scenevariable);
p2Scenevariable.disableSynchronization(); // Disable synchronization.
const p2GlobalVariable = new gdjs.Variable();
p2GlobalVariable.setString('This will be overriden');
p2RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p2GlobalVariable);
p2GlobalVariable.disableSynchronization(); // Disable synchronization.
p2RuntimeScene.renderAndStep(1000 / 60);
switchToPeer({
peerId: 'player-3',
otherPeerIds: ['player-1'],
playerNumber: 3,
});
// Create the variable on player 3 too.
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
const p3SceneVariable = new gdjs.Variable();
p3SceneVariable.setString('P3 is master of this variable');
p3RuntimeScene.getVariables().add('MyVariable', p3SceneVariable);
p3SceneVariable.disableSynchronization(); // Disable synchronization.
const p3GlobalVariable = new gdjs.Variable();
p3GlobalVariable.setString('P3 is master of this variable');
p3RuntimeScene
.getGame()
.getVariables()
.add('MyGlobalVariable', p3GlobalVariable);
p3GlobalVariable.disableSynchronization(); // Disable synchronization.
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Change the variables on player 3.
{
switchToPeer({
peerId: 'player-3',
otherPeerIds: ['player-1'],
playerNumber: 3,
});
p3RuntimeScene
.getVariables()
.get('MyVariable')
.setString('Changed by player 3');
p3RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.setString('Changed by player 3');
p3RuntimeScene.renderAndStep(1000 / 60);
}
// Change the variables on player 2.
{
switchToPeer({
peerId: 'player-2',
otherPeerIds: ['player-1'],
playerNumber: 2,
});
p2RuntimeScene
.getVariables()
.get('MyVariable')
.setString('Changed by player 2');
p2RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.setString('Changed by player 2');
p2RuntimeScene.renderAndStep(1000 / 60);
}
// Change the variables on player 1.
{
switchToPeer({
peerId: 'player-1',
otherPeerIds: ['player-2', 'player-3'],
playerNumber: 1,
});
p1RuntimeScene
.getVariables()
.get('MyVariable')
.setString('Changed by player 1');
p1RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.setString('Changed by player 1');
p1RuntimeScene.renderAndStep(1000 / 60);
}
// Check the variables have not been overriden on player 2.
{
switchToPeer({
peerId: 'player-2',
otherPeerIds: ['player-1'],
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
expect(
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Changed by player 2');
expect(
p2RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.getAsString()
).to.be('Changed by player 2');
}
// Check the variables have not been overriden on player 3.
{
switchToPeer({
peerId: 'player-3',
otherPeerIds: ['player-1'],
playerNumber: 3,
});
p3RuntimeScene.renderAndStep(1000 / 60);
expect(
p3RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Changed by player 3');
expect(
p3RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.getAsString()
).to.be('Changed by player 3');
}
// Check the variables have not been overriden on the host.
{
switchToPeer({
peerId: 'player-1',
otherPeerIds: ['player-2', 'player-3'],
playerNumber: 1,
});
p1RuntimeScene.renderAndStep(1000 / 60);
expect(
p1RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Changed by player 1');
expect(
p1RuntimeScene
.getGame()
.getVariables()
.get('MyGlobalVariable')
.getAsString()
).to.be('Changed by player 1');
}
});
it('synchronizes objects from the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -632,7 +968,7 @@ describe('Multiplayer', () => {
if (!p2Objects) throw new Error('No objects found');
expect(p2Objects.length).to.be(0);
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p2SpriteObject,
@@ -673,7 +1009,7 @@ describe('Multiplayer', () => {
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p2SpriteObject,
@@ -713,7 +1049,7 @@ describe('Multiplayer', () => {
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
if (!p2Objects) throw new Error('No objects found');
@@ -725,8 +1061,8 @@ describe('Multiplayer', () => {
it('synchronizes objects from a player to the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on a player:
switchToPeer({
@@ -796,7 +1132,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Move the object on the player:
{
@@ -848,7 +1184,7 @@ describe('Multiplayer', () => {
playerNumber: 3,
});
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p3SpriteObject,
@@ -922,15 +1258,15 @@ describe('Multiplayer', () => {
expect(p3ObjectsAndBehaviorsUpdated.length).to.be(0);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
});
it('allows ownership to change from host to a player to another player', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -987,8 +1323,8 @@ describe('Multiplayer', () => {
expect(p3SpriteObjectOriginal.getX()).to.be(142);
expect(p3SpriteObjectOriginal.getY()).to.be(143);
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Check player 3 can get ownership (and can directly move the instance, without waiting for the
// host to acknowledge the change).
@@ -1058,9 +1394,9 @@ describe('Multiplayer', () => {
expect(p2SpriteObject.getX()).to.be(342);
expect(p2SpriteObject.getY()).to.be(343);
markAllPeerEventsAsProcessed();
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
}
// Check player 2 can get ownership.
@@ -1144,7 +1480,7 @@ describe('Multiplayer', () => {
).to.be(2);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Check that the position given by player 2 is updated on the host and player 3.
{
@@ -1197,15 +1533,15 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getY()).to.be(243);
}
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
});
it('reconciles an instance owned by a player with a "ghost" instance created on other peers without a network ID (as not owned by them)', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on a player:
switchToPeer({
@@ -1277,7 +1613,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Now, create a new instance on the host and player 3, but owned by player 2.
// We call this in this test a "ghost" instance as it would be deleted if not "reconcilied".
@@ -1408,12 +1744,12 @@ describe('Multiplayer', () => {
expect(p3Object2.getX()).to.be(42);
expect(p3Object2.getY()).to.be(43);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
});
it('deletes an instance owned by another player after a bit (if not "reconciled" in the meantime)', async () => {
const { switchToPeer } = createP2PAndMultiplayerMessageManagerMock();
const { switchToPeer } = createMultiplayerManagersMock();
// Create an instance on a player (2), owned by another player (3).
// We can assume it's because there is some common logic running for all players
@@ -1465,8 +1801,8 @@ describe('Multiplayer', () => {
it('gives priority to the first ownership change and revert the wrong one', async () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -1539,7 +1875,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Now, try to change ownership to player 2 and 3 at the "same time".
{
@@ -1605,7 +1941,7 @@ describe('Multiplayer', () => {
p1SpriteMultiplayerObjectBehaviorUpdated.getPlayerObjectOwnership()
).to.be(2);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
// Wait so that player 3 retries.
@@ -1632,7 +1968,7 @@ describe('Multiplayer', () => {
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(3);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
await delay(210);
}
@@ -1657,7 +1993,7 @@ describe('Multiplayer', () => {
expect(
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(0);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
// Move the object on the player 2:
@@ -1724,7 +2060,7 @@ describe('Multiplayer', () => {
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(2);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
});
});
@@ -1743,7 +2079,7 @@ describe('Multiplayer', () => {
};
it('synchronizes scenes from the host to other players', async () => {
const { switchToPeer } = createP2PAndMultiplayerMessageManagerMock();
const { switchToPeer } = createMultiplayerManagersMock();
const gameLayoutData = [
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
@@ -1870,8 +2206,8 @@ describe('Multiplayer', () => {
it('reconciles a scene launched both by the host and by a player', async () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerMessageManagerMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
const gameLayoutData = [
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
@@ -1915,7 +2251,7 @@ describe('Multiplayer', () => {
p2RuntimeGame.getSceneStack().step(1000 / 60);
checkCurrentSceneIs(p2RuntimeGame, 'Scene1');
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Launch a second scene, first on the player:
p2RuntimeGame.getSceneStack().push('Scene2');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
namespace gdjs {
declare var cordova: any;
declare var SafariViewController: any;
const logger = new gdjs.Logger('Player Authentication');
const authComponents = gdjs.playerAuthenticationComponents;
@@ -15,16 +16,18 @@ namespace gdjs {
// Authentication display
let _authenticationWindow: Window | null = null; // For Web.
let _authenticationInAppWindow: any | null = null; // For Cordova.
let _authenticationRootContainer: HTMLDivElement | null = null;
let _authenticationLoaderContainer: HTMLDivElement | null = null;
let _authenticationIframeContainer: HTMLDivElement | null = null;
let _authenticationTextContainer: HTMLDivElement | null = null;
let _authenticationBanner: HTMLDivElement | null = null;
let _initialAuthenticationTimeoutId: NodeJS.Timeout | null = null;
let _automaticGamesPlatformAuthenticationTimeoutId: NodeJS.Timeout | null = null;
let _authenticationTimeoutId: NodeJS.Timeout | null = null;
// Communication methods.
let _automaticGamesPlatformAuthenticationCallback:
| ((event: MessageEvent) => void)
| null = null;
let _authenticationMessageCallback:
| ((event: MessageEvent) => void)
| null = null;
@@ -46,8 +49,10 @@ namespace gdjs {
// 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) => {
removeAutomaticGamesPlatformAuthenticationCallback(); // Remove any callback that could have been registered before.
_automaticGamesPlatformAuthenticationCallback = (
event: MessageEvent
) => {
receiveAuthenticationMessage({
runtimeScene,
event,
@@ -56,7 +61,7 @@ namespace gdjs {
};
window.addEventListener(
'message',
_authenticationMessageCallback,
_automaticGamesPlatformAuthenticationCallback,
true
);
logger.info(
@@ -69,9 +74,11 @@ namespace gdjs {
'*' // 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();
_automaticGamesPlatformAuthenticationTimeoutId = setTimeout(() => {
logger.info(
'Removing automatic games platform authentication listener.'
);
removeAutomaticGamesPlatformAuthenticationCallback();
}, 3000);
}
);
@@ -95,7 +102,7 @@ namespace gdjs {
connectionId ? `&connectionId=${connectionId}` : ''
}${
runtimeGame.isUsingGDevelopDevelopmentEnvironment() ? '&dev=true' : ''
}`;
}&allowLoginProviders=true`;
};
/**
@@ -104,7 +111,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 +126,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 +306,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 +317,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 +456,7 @@ namespace gdjs {
username: event.data.body.username,
userToken: event.data.body.token,
});
removeAuthenticationCallbacks();
removeAutomaticGamesPlatformAuthenticationCallback();
refreshAuthenticationBannerIfAny(runtimeScene);
break;
}
@@ -505,8 +514,6 @@ namespace gdjs {
* - the authentication window is closed
*/
const clearAuthenticationWindowTimeout = () => {
if (_initialAuthenticationTimeoutId)
clearTimeout(_initialAuthenticationTimeoutId);
if (_authenticationTimeoutId) clearTimeout(_authenticationTimeoutId);
};
@@ -707,8 +714,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 +731,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 +835,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 +902,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 +974,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 +1003,7 @@ namespace gdjs {
break;
}
if (isDimissedAlready) return;
if (isDismissedAlready) return;
if (status === 'dismissed') {
onAuthenticationContainerDismissed();
}
@@ -1106,6 +1064,24 @@ namespace gdjs {
}
};
/*
* Remove the automatic authentication callback when running on web.
*/
const removeAutomaticGamesPlatformAuthenticationCallback = function () {
if (_automaticGamesPlatformAuthenticationCallback) {
window.removeEventListener(
'message',
_automaticGamesPlatformAuthenticationCallback,
true
);
_automaticGamesPlatformAuthenticationCallback = null;
}
if (_automaticGamesPlatformAuthenticationTimeoutId) {
clearTimeout(_automaticGamesPlatformAuthenticationTimeoutId);
_automaticGamesPlatformAuthenticationTimeoutId = null;
}
};
/**
* Remove the banner displaying the authentication status.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,24 @@ Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
interface TopDownMovementNetworkSyncDataType {
a: float;
xv: float;
yv: float;
as: float;
lk: boolean;
rk: boolean;
uk: boolean;
dk: boolean;
wsu: boolean;
sa: float;
sf: float;
}
export interface TopDownMovementNetworkSyncData
extends BehaviorNetworkSyncData {
props: TopDownMovementNetworkSyncDataType;
}
/**
* Allows an object to move in 4 or 8 directions, with customizable speed, accelerations
* and rotation.
@@ -45,6 +63,10 @@ namespace gdjs {
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
_dontClearInputsBetweenFrames: boolean = false;
// This is useful when the object is synchronized over the network,
// object is controlled by the network and we want to ensure the current player
// cannot control it.
_ignoreDefaultControlsAsSyncedByNetwork: boolean = false;
// This is useful for extensions that need to know
// which keys were pressed and doesn't know the mapping
@@ -83,10 +105,11 @@ namespace gdjs {
this._movementAngleOffset = behaviorData.movementAngleOffset || 0;
}
getNetworkSyncData() {
getNetworkSyncData(): TopDownMovementNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
@@ -106,7 +129,9 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: BehaviorNetworkSyncData): void {
updateFromNetworkSyncData(
networkSyncData: TopDownMovementNetworkSyncData
): void {
super.updateFromNetworkSyncData(networkSyncData);
const behaviorSpecificProps = networkSyncData.props;
@@ -146,6 +171,8 @@ namespace gdjs {
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// And we are not using the default controls.
this._ignoreDefaultControlsAsSyncedByNetwork = true;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -324,19 +351,19 @@ namespace gdjs {
//Get the player input:
this._leftKey ||
(this._leftKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(LEFTKEY));
this._rightKey ||
(this._rightKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(RIGHTKEY));
this._downKey ||
(this._downKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(DOWNKEY));
this._upKey ||
(this._upKey =
!this._ignoreDefaultControls &&
!this.shouldIgnoreDefaultControls() &&
instanceContainer.getGame().getInputManager().isKeyPressed(UPKEY));
const elapsedTime = this.owner.getElapsedTime();
@@ -579,6 +606,13 @@ namespace gdjs {
this._ignoreDefaultControls = ignore;
}
shouldIgnoreDefaultControls() {
return (
this._ignoreDefaultControls ||
this._ignoreDefaultControlsAsSyncedByNetwork
);
}
simulateLeftKey() {
this._leftKey = true;
}

View File

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

View File

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

View File

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

View File

@@ -222,6 +222,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")

View File

@@ -37,6 +37,7 @@ struct PreviewExportOptions {
: project(project_),
exportPath(exportPath_),
useWindowMessageDebuggerClient(false),
nativeMobileApp(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
@@ -76,6 +77,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 +196,7 @@ struct PreviewExportOptions {
gd::String externalLayoutName;
gd::String fallbackAuthorUsername;
gd::String fallbackAuthorId;
bool nativeMobileApp;
std::map<gd::String, int> includeFileHashes;
bool projectDataOnlyExport;
bool fullLoadingScreen;

View File

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

View File

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

View File

@@ -32,6 +32,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. */
@@ -267,7 +269,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 +902,23 @@ namespace gdjs {
}
}
/**
* Helper function to get information about the platform running the game.
*/
getPlatformInfo = () => ({
// @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,
});
_setupGameVisibilityEvents() {
if (typeof navigator !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
@@ -954,6 +973,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 +990,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 +1230,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 +1271,22 @@ namespace gdjs {
if (syncData.ss) {
this._sceneStack.updateFromNetworkSyncData(syncData.ss);
}
if (syncData.extVar) {
for (const extensionName in syncData.extVar) {
if (!syncData.extVar.hasOwnProperty(extensionName)) {
continue;
}
const extensionVariablesData = syncData.extVar[extensionName];
const extensionVariables = this.getVariablesForExtension(
extensionName
);
if (extensionVariables) {
extensionVariables.updateFromNetworkSyncData(
extensionVariablesData
);
}
}
}
}
}
}

View File

@@ -453,7 +453,7 @@ 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];
@@ -463,7 +463,9 @@ namespace gdjs {
}
}
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 +487,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 +505,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 +525,24 @@ namespace gdjs {
this.hide(networkSyncData.hid);
}
if (
networkSyncData.lay !== undefined &&
this.layer !== networkSyncData.lay
) {
this.setLayer(networkSyncData.lay);
}
if (networkSyncData.if) {
// Force clear all forces and reapply them, using the garbage collector to recycle forces.
// Is that efficient?
this.clearForces();
for (let i = 0, len = networkSyncData.if.length; i < len; ++i) {
const forceData = networkSyncData.if[i];
const recycledOrNewForce = RuntimeObject.forcesGarbage.pop() as gdjs.Force;
const recycledOrNewForce = this._getRecycledForce(
forceData.x,
forceData.y,
forceData.m
);
recycledOrNewForce.updateFromNetworkSyncData(forceData);
this._instantForces.push(recycledOrNewForce);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3569,6 +3569,7 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions SetExternalLayoutName([Const] DOMString externalLayoutName);
[Ref] PreviewExportOptions SetIncludeFileHash([Const] DOMString includeFile, long hash);
[Ref] PreviewExportOptions SetProjectDataOnlyExport(boolean enable);
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);

View File

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

View File

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

View File

@@ -2732,6 +2732,7 @@ export class PreviewExportOptions extends EmscriptenObject {
setExternalLayoutName(externalLayoutName: string): PreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): PreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): PreviewExportOptions;
setNativeMobileApp(enable: boolean): PreviewExportOptions;
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;

View File

@@ -8,6 +8,7 @@ declare class gdPreviewExportOptions {
setExternalLayoutName(externalLayoutName: string): gdPreviewExportOptions;
setIncludeFileHash(includeFile: string, hash: number): gdPreviewExportOptions;
setProjectDataOnlyExport(enable: boolean): gdPreviewExportOptions;
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3407 6.4739C20.789 6.322 21.2756 6.56232 21.4275 7.01068C22.134 9.09598 21.9387 10.8304 21.0598 12.0678C20.1927 13.2886 18.7768 13.8571 17.3888 13.8571C16.039 13.8571 15.0362 13.48 14.1952 12.7894C13.4047 12.1402 12.79 11.2338 12.1615 10.3062C11.5644 9.42514 11.0121 8.90437 10.4432 8.59341C9.87912 8.28509 9.22524 8.14286 8.35349 8.14286C7.57792 8.14286 6.76118 8.57683 6.27981 9.42361C6.04587 9.83515 5.5226 9.97912 5.11106 9.74517C4.69952 9.51123 4.55555 8.98796 4.7895 8.57642C5.5639 7.21414 6.93813 6.42858 8.35349 6.42858C9.41788 6.42858 10.3774 6.60381 11.2654 7.08915C12.1485 7.57184 12.8869 8.3209 13.5806 9.34456C14.2429 10.3218 14.7207 11.0027 15.2832 11.4646C15.7953 11.8851 16.406 12.1429 17.3888 12.1429C18.3335 12.1429 19.1764 11.759 19.6622 11.0751C20.1363 10.4077 20.3881 9.28499 19.8039 7.56076C19.652 7.11241 19.8923 6.62581 20.3407 6.4739Z" fill="#000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.43276 12.2737C4.06677 11.6439 4.88063 11.4015 5.60704 11.4015C6.79924 11.4015 8.00928 11.8281 8.58396 13.0646C8.7233 13.3643 8.59323 13.7203 8.29346 13.8596C7.99368 13.999 7.63771 13.8689 7.49838 13.5691C7.19518 12.9168 6.54304 12.5986 5.60704 12.5986C5.13633 12.5986 4.64666 12.7552 4.27643 13.123C3.91237 13.4846 3.59854 14.1156 3.59854 15.1923C3.59854 16.2462 3.94405 16.8741 4.35034 17.2434C4.77081 17.6257 5.31729 17.7861 5.79326 17.7861C6.30247 17.7861 6.72159 17.6617 7.01762 17.3679C7.25418 17.1331 7.47817 16.7244 7.55807 16.0037H6.19229C5.86172 16.0037 5.59374 15.7357 5.59374 15.4051C5.59374 15.0746 5.86172 14.8066 6.19229 14.8066H8.18749C8.51806 14.8066 8.78604 15.0746 8.78604 15.4051C8.78604 16.6944 8.46334 17.6196 7.86096 18.2175C7.25915 18.8149 6.48116 18.9832 5.79326 18.9832C5.0721 18.9832 4.22195 18.7446 3.54507 18.1292C2.854 17.501 2.40143 16.5327 2.40143 15.1923C2.40143 13.8748 2.79257 12.9096 3.43276 12.2737Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,11 +1,79 @@
[
{
"id": "cameraParallax",
"titleByLocale": { "en": "Let's improve the camera and the background" },
"titleByLocale": {
"en": "Let's improve the camera and the background",
"fr": "Améliorons la caméra et l'arrière-plan",
"ar": "لنحسن الكاميرا والخلفية",
"de": "Verbessern wir die Kamera und den Hintergrund",
"es": "Mejoremos la cámara y el fondo",
"it": "Miglioriamo la fotocamera e lo sfondo",
"ja": "カメラと背景を改善しましょう",
"ko": "카메라와 배경을 개선합시다",
"pl": "Poprawmy kamerę i tło",
"pt": "Vamos melhorar a câmera e o fundo",
"th": "มาปรับปรุงกล้องและพื้นหลังกันเถอะ",
"ru": "Давайте улучшим камеру и фон",
"sl": "Izboljšajmo kamero in ozadje",
"sq": "Le të përmirësojmë kamerën dhe sfondin",
"uk": "Покращимо камеру та фон",
"zh": "让我们改进相机和背景"
},
"bulletPointsByLocale": [
{ "en": "Add a background with parallax effect" },
{ "en": "Add an extension" },
{ "en": "Use basic camera movements to follow the player" }
{
"en": "Add a background with parallax effect",
"fr": "Ajouter un arrière-plan avec effet de parallaxe",
"ar": "أضف خلفية مع تأثير المنظر المتحرك",
"de": "Füge einen Hintergrund mit Parallax-Effekt hinzu",
"es": "Agregar un fondo con efecto de paralaje",
"it": "Aggiungi uno sfondo con effetto parallasse",
"ja": "パララックス効果のある背景を追加する",
"ko": "패럴럭스 효과가 있는 배경 추가",
"pl": "Dodaj tło z efektem paralaksy",
"pt": "Adicionar um fundo com efeito de paralaxe",
"th": "เพิ่มพื้นหลังที่มีเอฟเฟกต์พารัลแลกซ์",
"ru": "Добавить фон с эффектом параллакса",
"sl": "Dodajte ozadje s paralaksnim učinkom",
"sq": "Shto një sfond me efekt paralaksi",
"uk": "Додайте фон з ефектом паралакса",
"zh": "添加具有视差效果的背景"
},
{
"en": "Add an extension",
"fr": "Ajouter une extension",
"ar": "أضف ملحق",
"de": "Eine Erweiterung hinzufügen",
"es": "Agregar una extensión",
"it": "Aggiungi un'estensione",
"ja": "拡張機能を追加する",
"ko": "확장 추가",
"pl": "Dodaj rozszerzenie",
"pt": "Adicionar uma extensão",
"th": "เพิ่มส่วนขยาย",
"ru": "Добавить расширение",
"sl": "Dodajte razširitev",
"sq": "Shto një zgjerim",
"uk": "Додайте розширення",
"zh": "添加扩展"
},
{
"en": "Use basic camera movements to follow the player",
"fr": "Utiliser des mouvements de caméra de base pour suivre le joueur",
"ar": "استخدم حركات الكاميرا الأساسية لمتابعة اللاعب",
"de": "Verwende grundlegende Kamerabewegungen, um dem Spieler zu folgen",
"es": "Usa movimientos básicos de cámara para seguir al jugador",
"it": "Usa movimenti di base della fotocamera per seguire il giocatore",
"ja": "プレイヤーを追うために基本的なカメラの動きを使用する",
"ko": "플레이어를 따라가는 기본적인 카메라 움직임을 사용하세요",
"pl": "Użyj podstawowych ruchów kamery, aby śledzić gracza",
"pt": "Use movimentos básicos de câmera para seguir o jogador",
"th": "ใช้การเคลื่อนไหวของกล้องพื้นฐานเพื่อติดตามผู้เล่น",
"ru": "Используйте базовые движения камеры, чтобы следовать за игроком",
"sl": "Uporabite osnovne premike kamere za sledenje igralcu",
"sq": "Përdorni lëvizjet themelore të kamerës për të ndjekur lojtarin",
"uk": "Використовуйте базові рухи камери для стеження за гравцем",
"zh": "使用基本的摄像机移动跟随玩家"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/cameraParallax.json",
"availableLocales": [
@@ -36,11 +104,130 @@
"isMiniTutorial": true
},
{
"id": "flingGame",
"titleByLocale": { "en": "Let's make a Fling Game" },
"id": "fireABullet",
"titleByLocale": { "en": "Let's fire a bullet" },
"bulletPointsByLocale": [
{ "en": "Learn to create a game from zero." },
{ "en": "Add a leaderboard to your game." }
{
"en": "Use a behavior to fire a bullet.",
"fr": "Utilisez un comportement pour tirer une balle.",
"ar": "استخدم سلوكًا لاطلاق النار بالرصاصة.",
"de": "Verwenden Sie ein Verhalten, um eine Kugel abzufeuern.",
"es": "Utiliza un comportamiento para disparar una bala.",
"it": "Usa un comportamento per sparare un proiettile.",
"ja": "行動を使用して弾を発射します。",
"ko": "행동을 사용하여 총알을 발사하십시오.",
"pl": "Użyj zachowania, aby strzelać pociskiem.",
"pt": "Use um comportamento para disparar uma bala.",
"th": "ใช้พฤติกรรมเพื่อยิงลูกพลัด.",
"ru": "Используйте поведение для выстрела пулей.",
"sl": "Uporabite vedenje za strel iz strele.",
"sq": "Përdorni një sjellje për të qëlluar një plumb.",
"uk": "Використовуйте поведінку для пострілу кулею.",
"zh": "使用行为来开火。"
},
{
"en": "Add an action to fire the bullet in the good direction.",
"fr": "Ajoutez une action pour tirer la balle dans la bonne direction.",
"ar": "أضف إجراءً لإطلاق الرصاصة في الاتجاه الصحيح.",
"de": "Fügen Sie eine Aktion hinzu, um die Kugel in die richtige Richtung abzufeuern.",
"es": "Agrega una acción para disparar la bala en la dirección correcta.",
"it": "Aggiungi un'azione per sparare il proiettile nella giusta direzione.",
"ja": "良い方向に弾を発射するためのアクションを追加します。",
"ko": "좋은 방향으로 총알을 발사하기 위한 동작을 추가하십시오.",
"pl": "Dodaj akcję, aby strzelać pociskiem w dobrym kierunku.",
"pt": "Adicione uma ação para disparar a bala na direção correta.",
"th": "เพิ่มการกระทำเพื่อยิงกระสุนในทิศทางที่ดี",
"ru": "Добавьте действие для выстрела пулей в нужном направлении.",
"sl": "Dodajte dejanje za streljanje krogle v pravo smer.",
"sq": "Shtoni një veprim për të qëlluar plumbin në drejtimin e duhur.",
"uk": "Додайте дію для вистрілу кулею в потрібному напрямку.",
"zh": "添加一个动作,以正确的方向射击子弹。"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/fireABullet.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/fireABullet/game.json",
"initialProjectData": {
"player": "Player",
"bullet": "Bullet",
"PlayScene": "PlayScene"
},
"isMiniTutorial": true
},
{
"id": "flingGame",
"titleByLocale": {
"en": "Let's make a Fling Game",
"fr": "Faisons un jeu de lancer",
"ar": "لنصنع لعبة رمي",
"de": "Lass uns ein Schleuder-Spiel machen",
"es": "Hagamos un juego de lanzamiento",
"it": "Facciamo un gioco di lancio",
"ja": "フリングゲームを作ろう",
"ko": "플링 게임을 만들어 보자",
"pl": "Zróbmy grę w rzucanie",
"pt": "Vamos fazer um jogo de arremesso",
"th": "มาทำเกมขว้างกันเถอะ",
"ru": "Давайте сделаем игру бросков",
"sl": "Naredimo igro metanja",
"sq": "Le të bëjmë një lojë hedhjeje",
"uk": "Зробимо гру кидання",
"zh": "让我们制作一个弹射游戏"
},
"bulletPointsByLocale": [
{
"en": "Learn to create a game from zero.",
"fr": "Apprenez à créer un jeu à partir de zéro.",
"ar": "تعلم إنشاء لعبة من الصفر.",
"de": "Lerne, ein Spiel von Grund auf zu erstellen.",
"es": "Aprende a crear un juego desde cero.",
"it": "Impara a creare un gioco da zero.",
"ja": "ゼロからゲームを作る方法を学びます。",
"ko": "게임을 처음부터 만드는 방법을 배우세요.",
"pl": "Naucz się tworzyć grę od podstaw.",
"pt": "Aprenda a criar um jogo do zero.",
"th": "เรียนรู้วิธีสร้างเกมตั้งแต่เริ่มต้น",
"ru": "Научитесь создавать игру с нуля.",
"sl": "Naučite se ustvariti igro iz nič.",
"sq": "Mësoni të krijoni një lojë nga zeroja.",
"uk": "Навчіться створювати гру з нуля.",
"zh": "学习从零开始创建游戏。"
},
{
"en": "Add a leaderboard to your game.",
"fr": "Ajoutez un classement à votre jeu.",
"ar": "أضف لوحة المتصدرين إلى لعبتك.",
"de": "Füge deinem Spiel eine Rangliste hinzu.",
"es": "Agrega una tabla de clasificación a tu juego.",
"it": "Aggiungi una classifica al tuo gioco.",
"ja": "ゲームにリーダーボードを追加します。",
"ko": "게임에 리더보드를 추가하세요.",
"pl": "Dodaj tabelę wyników do swojej gry.",
"pt": "Adicione uma tabela de classificação ao seu jogo.",
"th": "เพิ่มกระดานผู้นำในเกมของคุณ",
"ru": "Добавьте таблицу лидеров в вашу игру.",
"sl": "Dodajte lestvico najboljših svoji igri.",
"sq": "Shto një tabelë të drejtuesve në lojën tuaj.",
"uk": "Додайте таблицю лідерів до вашої гри.",
"zh": "为您的游戏添加排行榜。"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/flingGame.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
@@ -49,11 +236,60 @@
{
"id": "healthBar",
"titleByLocale": {
"en": "Let's communicate to the player the remaining health points"
"en": "Let's communicate to the player the remaining health points",
"fr": "Communiquons au joueur les points de vie restants",
"ar": "لنخبر اللاعب بنقاط الصحة المتبقية",
"de": "Teilen wir dem Spieler die verbleibenden Gesundheitspunkte mit",
"es": "Comuniquemos al jugador los puntos de salud restantes",
"it": "Comunichiamo al giocatore i punti salute rimanenti",
"ja": "プレイヤーに残りの体力ポイントを伝えましょう",
"ko": "플레이어에게 남은 체력 포인트를 알려줍시다",
"pl": "Przekażmy graczowi pozostałe punkty zdrowia",
"pt": "Vamos comunicar ao jogador os pontos de saúde restantes",
"th": "สื่อสารกับผู้เล่นเกี่ยวกับแต้มสุขภาพที่เหลืออยู่",
"ru": "Сообщим игроку оставшиеся очки здоровья",
"sl": "Sporočimo igralcu preostale točke zdravja",
"sq": "Le të komunikojmë me lojtarin pikët e mbetura të shëndetit",
"uk": "Повідомимо гравцеві про залишок очок здоров'я",
"zh": "让我们告诉玩家剩余的健康点数"
},
"bulletPointsByLocale": [
{ "en": "Use a prefab for a health bar" },
{ "en": "Update the health bar based on the player's health" }
{
"en": "Use a prefab for a health bar",
"fr": "Utilisez un préfabriqué pour une barre de santé",
"ar": "استخدم نموذجاً جاهزاً لشريط الصحة",
"de": "Verwende ein vorgefertigtes Modell für eine Gesundheitsleiste",
"es": "Usa un prefab para una barra de salud",
"it": "Usa un prefab per una barra della salute",
"ja": "ヘルスバーのプレハブを使用する",
"ko": "체력 바를 위한 프리팹 사용",
"pl": "Użyj prefabrykatu dla paska zdrowia",
"pt": "Use um prefab para uma barra de saúde",
"th": "ใช้พรีแฟบสำหรับแถบสุขภาพ",
"ru": "Используйте префаб для индикатора здоровья",
"sl": "Uporabite predlogo za zdravstveno vrstico",
"sq": "Përdorni një prefab për një shirit shëndeti",
"uk": "Використовуйте префаб для індикатора здоров'я",
"zh": "使用预制件作为健康条"
},
{
"en": "Update the health bar based on the player's health",
"fr": "Mettre à jour la barre de santé en fonction de la santé du joueur",
"ar": "تحديث شريط الصحة بناءً على صحة اللاعب",
"de": "Aktualisiere die Gesundheitsleiste basierend auf der Gesundheit des Spielers",
"es": "Actualizar la barra de salud según la salud del jugador",
"it": "Aggiorna la barra della salute in base alla salute del giocatore",
"ja": "プレイヤーの健康に基づいてヘルスバーを更新する",
"ko": "플레이어의 체력에 따라 체력 바를 업데이트하세요",
"pl": "Aktualizuj pasek zdrowia w oparciu o zdrowie gracza",
"pt": "Atualize a barra de saúde com base na saúde do jogador",
"th": "อัปเดตแถบสุขภาพตามสุขภาพของผู้เล่น",
"ru": "Обновите индикатор здоровья в зависимости от здоровья игрока",
"sl": "Posodobite zdravstveno vrstico glede na zdravje igralca",
"sq": "Përditësoni shiritin e shëndetit bazuar në shëndetin e lojtarit",
"uk": "Оновіть індикатор здоров'я відповідно до здоров'я гравця",
"zh": "根据玩家的健康状况更新健康条"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/healthBar.json",
"availableLocales": [
@@ -80,10 +316,61 @@
},
{
"id": "joystick",
"titleByLocale": { "en": "Let's add mobile controls to our game" },
"titleByLocale": {
"en": "Let's add mobile controls to our game",
"fr": "Ajoutons des contrôles mobiles à notre jeu",
"ar": "لنضيف عناصر التحكم بالهاتف المحمول إلى لعبتنا",
"de": "Fügen wir unserem Spiel mobile Steuerungen hinzu",
"es": "Agreguemos controles móviles a nuestro juego",
"it": "Aggiungiamo i controlli mobili al nostro gioco",
"ja": "ゲームにモバイルコントロールを追加しましょう",
"ko": "게임에 모바일 컨트롤을 추가합시다",
"pl": "Dodajmy sterowanie mobilne do naszej gry",
"pt": "Vamos adicionar controles móveis ao nosso jogo",
"th": "มาเพิ่มการควบคุมมือถือในเกมของเรากันเถอะ",
"ru": "Добавим мобильное управление в нашу игру",
"sl": "Dodajmo mobilne kontrole naši igri",
"sq": "Le të shtojmë kontrollin e telefonit në lojën tonë",
"uk": "Додамо мобільні елементи керування до нашої гри",
"zh": "让我们为游戏添加移动控制"
},
"bulletPointsByLocale": [
{ "en": "Add a joystick prefab" },
{ "en": "Add a behavior" }
{
"en": "Add a joystick prefab",
"fr": "Ajoutez un préfabriqué de joystick",
"ar": "أضف نموذج عصا التحكم الجاهز",
"de": "Füge ein Joystick-Prefab hinzu",
"es": "Agrega un prefab de joystick",
"it": "Aggiungi un prefab di joystick",
"ja": "ジョイスティックのプレハブを追加する",
"ko": "조이스틱 프리팹 추가",
"pl": "Dodaj prefabrykowany joystick",
"pt": "Adicione um prefab de joystick",
"th": "เพิ่มจอยสติ๊กพรีแฟบ",
"ru": "Добавить префаб джойстика",
"sl": "Dodajte predlogo za krmilno palico",
"sq": "Shto një prefab joystick",
"uk": "Додайте префаб джойстика",
"zh": "添加一个操纵杆预制件"
},
{
"en": "Add a behavior",
"fr": "Ajoutez un comportement",
"ar": "أضف سلوكًا",
"de": "Füge ein Verhalten hinzu",
"es": "Agrega un comportamiento",
"it": "Aggiungi un comportamento",
"ja": "動作を追加する",
"ko": "행동 추가",
"pl": "Dodaj zachowanie",
"pt": "Adicione um comportamento",
"th": "เพิ่มพฤติกรรม",
"ru": "Добавить поведение",
"sl": "Dodajte vedenje",
"sq": "Shto një sjellje",
"uk": "Додайте поведінку",
"zh": "添加行为"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/joystick.json",
"availableLocales": [
@@ -113,13 +400,65 @@
},
{
"id": "knightPlatformer",
"titleByLocale": { "en": "Let's make a platformer game" },
"titleByLocale": {
"en": "Let's make a platformer game",
"fr": "Faisons un jeu de plateforme",
"ar": "لنصنع لعبة بلاتفورمر",
"de": "Lass uns ein Plattformspiel machen",
"es": "Hagamos un juego de plataformas",
"it": "Facciamo un gioco platform",
"ja": "プラットフォーマーゲームを作ろう",
"ko": "플랫폼 게임을 만들어 보자",
"pl": "Zróbmy grę platformową",
"pt": "Vamos fazer um jogo de plataforma",
"th": "มาทำเกมแพลตฟอร์มกันเถอะ",
"ru": "Сделаем платформер",
"sl": "Naredimo igro platforme",
"sq": "Le të bëjmë një lojë platformë",
"uk": "Зробимо платформер",
"zh": "让我们制作一个平台游戏"
},
"bulletPointsByLocale": [
{ "en": "Use a behavior to mark an object as a platform." },
{ "en": "Use another behavior to control the player." }
{
"en": "Use a behavior to mark an object as a platform.",
"fr": "Utilisez un comportement pour marquer un objet comme une plateforme.",
"ar": "استخدم سلوكًا لتحديد كائن كمنصة.",
"de": "Verwende ein Verhalten, um ein Objekt als Plattform zu markieren.",
"es": "Usa un comportamiento para marcar un objeto como plataforma.",
"it": "Usa un comportamento per segnare un oggetto come piattaforma.",
"ja": "オブジェクトをプラットフォームとしてマークするために動作を使用します。",
"ko": "객체를 플랫폼으로 표시하는 동작을 사용합니다.",
"pl": "Użyj zachowania, aby oznaczyć obiekt jako platformę.",
"pt": "Use um comportamento para marcar um objeto como plataforma.",
"th": "ใช้พฤติกรรมเพื่อทำเครื่องหมายวัตถุเป็นแพลตฟอร์ม",
"ru": "Используйте поведение, чтобы отметить объект как платформу.",
"sl": "Uporabite vedenje za označitev predmeta kot platformo.",
"sq": "Përdorni një sjellje për të shënuar një objekt si një platformë.",
"uk": "Використовуйте поведінку, щоб позначити об'єкт як платформу.",
"zh": "使用行为将对象标记为平台。"
},
{
"en": "Use another behavior to control the player.",
"fr": "Utilisez un autre comportement pour contrôler le joueur.",
"ar": "استخدم سلوكًا آخر للتحكم في اللاعب.",
"de": "Verwende ein anderes Verhalten, um den Spieler zu steuern.",
"es": "Usa otro comportamiento para controlar al jugador.",
"it": "Usa un altro comportamento per controllare il giocatore.",
"ja": "プレイヤーを操作するために別の動作を使用します。",
"ko": "플레이어를 제어하기 위해 다른 행동을 사용합니다.",
"pl": "Użyj innego zachowania, aby kontrolować gracza.",
"pt": "Use outro comportamento para controlar o jogador.",
"th": "ใช้พฤติกรรมอื่นในการควบคุมผู้เล่น",
"ru": "Используйте другое поведение, чтобы управлять игроком.",
"sl": "Uporabite drugo vedenje za nadzor igralca.",
"sq": "Përdorni një sjellje tjetër për të kontrolluar lojtarin.",
"uk": "Використовуйте іншу поведінку для керування гравцем.",
"zh": "使用另一种行为来控制玩家。"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/knightPlatformer.json",
"availableLocales": ["en"],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/knightPlatformer/game.json",
"initialProjectData": {
"playerHitBox": "KnightHitBox",
"tiles": "Tiles",
@@ -129,10 +468,61 @@
},
{
"id": "object3d",
"titleByLocale": { "en": "Let's add a 3D object to our game" },
"titleByLocale": {
"en": "Let's add a 3D object to our game",
"fr": "Ajoutons un objet 3D à notre jeu",
"ar": "لنضيف كائن ثلاثي الأبعاد إلى لعبتنا",
"de": "Fügen wir unserem Spiel ein 3D-Objekt hinzu",
"es": "Agreguemos un objeto 3D a nuestro juego",
"it": "Aggiungiamo un oggetto 3D al nostro gioco",
"ja": "ゲームに3Dオブジェクトを追加しましょう",
"ko": "게임에 3D 객체를 추가합시다",
"pl": "Dodajmy obiekt 3D do naszej gry",
"pt": "Vamos adicionar um objeto 3D ao nosso jogo",
"th": "มาเพิ่มวัตถุ 3 มิติในเกมของเรากันเถอะ",
"ru": "Добавим 3D объект в нашу игру",
"sl": "Dodajmo 3D predmet v našo igro",
"sq": "Le të shtojmë një objekt 3D në lojën tonë",
"uk": "Додамо 3D об'єкт до нашої гри",
"zh": "让我们在游戏中添加一个3D对象"
},
"bulletPointsByLocale": [
{ "en": "Add a 3D Box" },
{ "en": "Add a behavior" }
{
"en": "Add a 3D Box",
"fr": "Ajoutez une boîte 3D",
"ar": "أضف صندوق ثلاثي الأبعاد",
"de": "Füge eine 3D-Box hinzu",
"es": "Agrega una caja 3D",
"it": "Aggiungi una scatola 3D",
"ja": "3Dボックスを追加する",
"ko": "3D 박스를 추가하세요",
"pl": "Dodaj pudełko 3D",
"pt": "Adicione uma caixa 3D",
"th": "เพิ่มกล่อง 3 มิติ",
"ru": "Добавить 3D ящик",
"sl": "Dodajte 3D škatlo",
"sq": "Shto një kuti 3D",
"uk": "Додайте 3D ящик",
"zh": "添加一个3D盒子"
},
{
"en": "Add a behavior",
"fr": "Ajoutez un comportement",
"ar": "أضف سلوكًا",
"de": "Füge ein Verhalten hinzu",
"es": "Agrega un comportamiento",
"it": "Aggiungi un comportamento",
"ja": "動作を追加する",
"ko": "행동 추가",
"pl": "Dodaj zachowanie",
"pt": "Adicione um comportamento",
"th": "เพิ่มพฤติกรรม",
"ru": "Добавить поведение",
"sl": "Dodajte vedenje",
"sq": "Shto një sjellje",
"uk": "Додайте поведінку",
"zh": "添加行为"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/object3d.json",
"availableLocales": [
@@ -159,11 +549,79 @@
},
{
"id": "plinkoMultiplier",
"titleByLocale": { "en": "Let's improve a scoring system" },
"titleByLocale": {
"en": "Let's improve a scoring system",
"fr": "Améliorons un système de score",
"ar": "لنحسن نظام تسجيل النقاط",
"de": "Verbessern wir ein Punktesystem",
"es": "Mejoremos un sistema de puntuación",
"it": "Miglioriamo un sistema di punteggio",
"ja": "スコアリングシステムを改善しましょう",
"ko": "점수 시스템을 개선합시다",
"pl": "Ulepszmy system punktacji",
"pt": "Vamos melhorar um sistema de pontuação",
"th": "มาปรับปรุงระบบการให้คะแนนกันเถอะ",
"ru": "Улучшите систему подсчета очков",
"sl": "Izboljšajmo sistem točkovanja",
"sq": "Le të përmirësojmë një sistem të pikëzimit",
"uk": "Покращимо систему підрахунку очок",
"zh": "改进记分系统"
},
"bulletPointsByLocale": [
{ "en": "Making objects disappear or appear when colliding" },
{ "en": "Creating, modifying and accessing a scene variable" },
{ "en": "Updating a score accordingly" }
{
"en": "Make objects disappear or appear when colliding",
"fr": "Faire disparaître ou apparaître des objets lors d'une collision",
"ar": "جعل الكائنات تختفي أو تظهر عند الاصطدام",
"de": "Objekte beim Zusammenstoß verschwinden oder erscheinen lassen",
"es": "Hacer que los objetos desaparezcan o aparezcan al chocar",
"it": "Far scomparire o apparire oggetti in caso di collisione",
"ja": "衝突時にオブジェクトを消去または表示する",
"ko": "충돌 시 객체를 사라지게 하거나 나타나게 하기",
"pl": "Spraw, aby obiekty znikały lub pojawiały się przy kolizji",
"pt": "Fazer objetos desaparecerem ou aparecerem ao colidir",
"th": "ทำให้วัตถุหายไปหรือปรากฏขึ้นเมื่อชนกัน",
"ru": "Заставлять объекты исчезать или появляться при столкновении",
"sl": "Naj predmeti izginejo ali se pojavijo ob trku",
"sq": "Bëj që objektet të zhduken ose të shfaqen kur përplasen",
"uk": "Змушуйте об'єкти зникати або з'являтися при зіткненні",
"zh": "在碰撞时使对象消失或出现"
},
{
"en": "Create, modify and access a scene variable",
"fr": "Créer, modifier et accéder à une variable de scène",
"ar": "إنشاء وتعديل والوصول إلى متغير المشهد",
"de": "Erstellen, ändern und auf eine Szenenvariable zugreifen",
"es": "Crear, modificar y acceder a una variable de escena",
"it": "Creare, modificare e accedere a una variabile di scena",
"ja": "シーン変数を作成、変更、およびアクセスする",
"ko": "씬 변수를 생성, 수정 및 액세스",
"pl": "Tworzenie, modyfikowanie i dostęp do zmiennej sceny",
"pt": "Criar, modificar e acessar uma variável de cena",
"th": "สร้าง แก้ไข และเข้าถึงตัวแปรฉาก",
"ru": "Создание, изменение и доступ к переменной сцены",
"sl": "Ustvarite, spremenite in dostopajte do spremenljivke scene",
"sq": "Krijoni, modifikoni dhe aksesoni një variabël të skenës",
"uk": "Створюйте, змінюйте та отримуйте доступ до змінної сцени",
"zh": "创建、修改和访问场景变量"
},
{
"en": "Update a score accordingly",
"fr": "Mettre à jour un score en conséquence",
"ar": "تحديث النتيجة وفقًا لذلك",
"de": "Passen Sie den Punktestand entsprechend an",
"es": "Actualizar una puntuación en consecuencia",
"it": "Aggiorna di conseguenza un punteggio",
"ja": "スコアを適宜更新する",
"ko": "점수를 적절히 업데이트",
"pl": "Aktualizuj wynik odpowiednio",
"pt": "Atualizar uma pontuação de acordo",
"th": "อัปเดตคะแนนตามนั้น",
"ru": "Обновите счет соответственно",
"sl": "Posodobite rezultat ustrezno",
"sq": "Përditësoni një rezultat në përputhje me rrethanat",
"uk": "Відповідно оновлюйте оцінку",
"zh": "相应地更新分数"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/plinkoMultiplier.json",
"availableLocales": [
@@ -196,11 +654,79 @@
},
{
"id": "timer",
"titleByLocale": { "en": "Let's use time to measure a score" },
"titleByLocale": {
"en": "Let's use time to measure a score",
"fr": "Utilisons le temps pour mesurer un score",
"ar": "لنستخدم الوقت لقياس النتيجة",
"de": "Lass uns die Zeit nutzen, um eine Punktzahl zu messen",
"es": "Usemos el tiempo para medir una puntuación",
"it": "Usiamo il tempo per misurare un punteggio",
"ja": "スコアを測定するために時間を使いましょう",
"ko": "시간을 사용하여 점수를 측정합시다",
"pl": "Użyjmy czasu do pomiaru wyniku",
"pt": "Vamos usar o tempo para medir uma pontuação",
"th": "มาใช้เวลาในการวัดคะแนนกันเถอะ",
"ru": "Используем время для измерения счета",
"sl": "Uporabimo čas za merjenje rezultata",
"sq": "Le të përdorim kohën për të matur një rezultat",
"uk": "Використаймо час для вимірювання результату",
"zh": "让我们用时间来衡量分数"
},
"bulletPointsByLocale": [
{ "en": "Create and modify a text" },
{ "en": "Start a timer" },
{ "en": "Use the timer to display a score" }
{
"en": "Create and modify a text",
"fr": "Créer et modifier un texte",
"ar": "إنشاء نص وتعديله",
"de": "Einen Text erstellen und ändern",
"es": "Crear y modificar un texto",
"it": "Creare e modificare un testo",
"ja": "テキストを作成および変更する",
"ko": "텍스트를 생성하고 수정하기",
"pl": "Utwórz i zmodyfikuj tekst",
"pt": "Criar e modificar um texto",
"th": "สร้างและแก้ไขข้อความ",
"ru": "Создавать и изменять текст",
"sl": "Ustvarite in spremenite besedilo",
"sq": "Krijoni dhe modifikoni një tekst",
"uk": "Створювати та змінювати текст",
"zh": "创建和修改文本"
},
{
"en": "Start a timer",
"fr": "Démarrer un minuteur",
"ar": "بدء مؤقت",
"de": "Einen Timer starten",
"es": "Iniciar un temporizador",
"it": "Avviare un timer",
"ja": "タイマーを開始する",
"ko": "타이머 시작하기",
"pl": "Uruchom minutnik",
"pt": "Iniciar um temporizador",
"th": "เริ่มจับเวลา",
"ru": "Запустить таймер",
"sl": "Začnite časovnik",
"sq": "Nisni një kronometër",
"uk": "Запустити таймер",
"zh": "启动计时器"
},
{
"en": "Use the timer to display a score",
"fr": "Utiliser le minuteur pour afficher un score",
"ar": "استخدم المؤقت لعرض النتيجة",
"de": "Verwenden Sie den Timer, um eine Punktzahl anzuzeigen",
"es": "Usar el temporizador para mostrar una puntuación",
"it": "Usa il timer per visualizzare un punteggio",
"ja": "タイマーを使用してスコアを表示する",
"ko": "타이머를 사용하여 점수 표시하기",
"pl": "Użyj minutnika do wyświetlania wyniku",
"pt": "Usar o temporizador para exibir uma pontuação",
"th": "ใช้ตัวจับเวลาเพื่อแสดงคะแนน",
"ru": "Используйте таймер для отображения счета",
"sl": "Uporabite časovnik za prikaz rezultata",
"sq": "Përdorni kronometrin për të shfaqur një rezultat",
"uk": "Використовуйте таймер для відображення рахунку",
"zh": "使用计时器显示分数"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/timer.json",
"availableLocales": [
@@ -224,5 +750,69 @@
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/timer/game.json",
"initialProjectData": { "gameScene": "GameScene" },
"isMiniTutorial": true
},
{
"id": "topDownRPGMovement",
"titleByLocale": { "en": "Let's make a Top-Down RPG game" },
"bulletPointsByLocale": [
{
"en": "Use a behavior to control a top-down character.",
"fr": "Utilisez un comportement pour contrôler un personnage de haut en bas.",
"ar": "استخدم سلوكًا للتحكم في شخصية من الأعلى إلى الأسفل.",
"de": "Verwenden Sie ein Verhalten, um eine Top-Down-Figur zu steuern.",
"es": "Utilice un comportamiento para controlar un personaje desde arriba hacia abajo.",
"it": "Usa un comportamento per controllare un personaggio top-down.",
"ja": "トップダウンキャラクターを制御するための行動を使用します。",
"ko": "탑다운 캐릭터를 제어하기 위한 동작을 사용하십시오.",
"pl": "Użyj zachowania do kontrolowania postaci z góry na dół.",
"pt": "Use um comportamento para controlar um personagem de cima para baixo.",
"th": "ใช้พฤติกรรมเพื่อควบคุมตัวละครแบบ top-down",
"ru": "Используйте поведение для управления персонажем сверху вниз.",
"sl": "Uporabite vedenje za nadzor lika od zgoraj navzdol.",
"sq": "Përdorni një sjellje për të kontrolluar një personazh nga lart-dhe-poshtë.",
"uk": "Використовуйте поведінку для управління персонажем зверху донизу.",
"zh": "使用一种行为来控制自上而下的角色。"
},
{
"en": "Use another behavior for the pixel perfect movement.",
"fr": "Utilisez un autre comportement pour le mouvement pixel parfait.",
"ar": "استخدم سلوكًا آخر للحركة المثالية بالبكسل.",
"de": "Verwenden Sie ein weiteres Verhalten für die pixelgenaue Bewegung.",
"es": "Utiliza otro comportamiento para el movimiento perfecto por píxeles.",
"it": "Usa un altro comportamento per il movimento perfetto a livello di pixel.",
"ja": "ピクセル完璧な移動のために別の行動を使用します。",
"ko": "픽셀 완벽한 움직임을 위해 다른 동작을 사용하십시오.",
"pl": "Użyj innego zachowania do idealnego ruchu pikselowego.",
"pt": "Use outro comportamento para o movimento perfeito por pixels.",
"th": "ใช้พฤติกรรมอื่นสำหรับการเคลื่อนที่ที่ละเอียดพิกเซล",
"ru": "Используйте другое поведение для пиксельно точного движения.",
"sl": "Uporabite drugo vedenje za gibanje z natančnostjo na pike.",
"sq": "Përdorni një sjellje tjetër për lëvizjen perfekte me piksel.",
"uk": "Використовуйте інше поведінку для піксельно точного руху.",
"zh": "使用另一种行为进行像素完美的移动。"
}
],
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/topDownRPGMovement.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/topDownRPGMovement/game.json",
"initialProjectData": { "player": "Player", "scene1": "Overworld" },
"isMiniTutorial": true
}
]

View File

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

View File

@@ -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';
@@ -1035,7 +1036,7 @@ const EventsFunctionsList = React.forwardRef<
{
isRoot: true,
content: new LabelTreeViewItemContent(
extensionBehaviorsRootFolderId,
extensionConfigurationRootFolderId,
i18n._(t`Extension`)
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1053,7 +1054,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 +1062,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 +1253,7 @@ const EventsFunctionsList = React.forwardRef<
extensionObjectsRootFolderId,
extensionBehaviorsRootFolderId,
extensionFunctionsRootFolderId,
extensionConfigurationRootFolderId,
...objectTreeViewItems.map(item => item.content.getId()),
...behaviorTreeViewItems.map(item => item.content.getId()),
];

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ 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) {
@@ -84,4 +84,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
export const renderInlineGlobalVariable = (
props: ParameterInlineRendererProps
) => renderVariableWithIcon(props, 'global variable', GlobalIcon);
) => renderVariableWithIcon(props, 'global variable', GlobalVariableIcon);

View File

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

View File

@@ -14,7 +14,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';
@@ -178,4 +178,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
export const renderInlineObjectVariable = (
props: ParameterInlineRendererProps
) => renderVariableWithIcon(props, 'object variable', ObjectIcon);
) => renderVariableWithIcon(props, 'object variable', ObjectVariableIcon);

View File

@@ -12,7 +12,7 @@ import {
type FieldFocusFunction,
} from './ParameterFieldCommons';
import { enumerateVariables } from './EnumerateVariables';
import SceneIcon from '../../UI/CustomSvgIcons/Scene';
import SceneVariableIcon from '../../UI/CustomSvgIcons/SceneVariable';
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
function SceneVariableField(props: ParameterFieldProps, ref) {
@@ -91,4 +91,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
export const renderInlineSceneVariable = (
props: ParameterInlineRendererProps
) => renderVariableWithIcon(props, 'scene variable', SceneIcon);
) => renderVariableWithIcon(props, 'scene variable', SceneVariableIcon);

View File

@@ -34,10 +34,10 @@ import VariableStructureIcon from '../../VariablesList/Icons/VariableStructureIc
import UnknownTypeIcon from '../../UI/CustomSvgIcons/Cross';
import { type EnumeratedVariable } from './EnumerateVariables';
import { LineStackLayout } from '../../UI/Layout';
import GlobalIcon from '../../UI/CustomSvgIcons/Publish';
import SceneIcon from '../../UI/CustomSvgIcons/Scene';
import ObjectIcon from '../../UI/CustomSvgIcons/Object';
import LocalIcon from '../../UI/CustomSvgIcons/ExternalEvents';
import GlobalVariableIcon from '../../UI/CustomSvgIcons/GlobalVariable';
import SceneVariableIcon from '../../UI/CustomSvgIcons/SceneVariable';
import ObjectVariableIcon from '../../UI/CustomSvgIcons/ObjectVariable';
import LocalVariableIcon from '../../UI/CustomSvgIcons/LocalVariable';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope.flow';
const gd: libGDevelop = global.gd;
@@ -128,14 +128,14 @@ export const getVariableSourceIcon = (
switch (variableSourceType) {
case gd.VariablesContainer.Global:
case gd.VariablesContainer.ExtensionGlobal:
return GlobalIcon;
return GlobalVariableIcon;
case gd.VariablesContainer.Scene:
case gd.VariablesContainer.ExtensionScene:
return SceneIcon;
return SceneVariableIcon;
case gd.VariablesContainer.Object:
return ObjectIcon;
return ObjectVariableIcon;
case gd.VariablesContainer.Local:
return LocalIcon;
return LocalVariableIcon;
default:
return UnknownTypeIcon;
}

View File

@@ -17,7 +17,7 @@ import RedoIcon from '../UI/CustomSvgIcons/Redo';
import ToolbarSearchIcon from '../UI/CustomSvgIcons/ToolbarSearch';
import EditSceneIcon from '../UI/CustomSvgIcons/EditScene';
import { getShortcutDisplayName, useShortcutMap } from '../KeyboardShortcuts';
import AddLocalVariableIcon from '../UI/CustomSvgIcons/ExternalEvents';
import AddLocalVariableIcon from '../UI/CustomSvgIcons/LocalVariable';
type Props = {|
onAddStandardEvent: () => void,

View File

@@ -874,7 +874,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
label: i18n._(t`Add`),
submenu: [
{
label: i18n._(t`Add New Event Below`),
label: i18n._(t`New Event Below`),
click: () => {
this.addNewEvent('BuiltinCommonInstructions::Standard');
},
@@ -883,7 +883,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
),
},
{
label: i18n._(t`Add Sub Event`),
label: i18n._(t`Sub Event`),
click: () => this.addSubEvent(),
enabled: this._selectionCanHaveSubEvents(),
accelerator: getShortcutDisplayName(
@@ -891,7 +891,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
),
},
{
label: i18n._(t`Add Local Variable`),
label: i18n._(t`Local Variable`),
click: () => this.addLocalVariable(),
enabled: this._selectionCanHaveLocalVariables(),
accelerator: getShortcutDisplayName(
@@ -899,7 +899,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
),
},
{
label: i18n._(t`Add Comment`),
label: i18n._(t`Comment`),
click: () => {
this.addNewEvent('BuiltinCommonInstructions::Comment');
},
@@ -914,7 +914,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
metadata.type !== 'BuiltinCommonInstructions::Comment'
)
.map(metadata => ({
label: i18n._(t`Add ${metadata.fullName}`),
label: metadata.fullName,
click: () => {
this.addNewEvent(metadata.type);
},
@@ -1927,6 +1927,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
<EventsTree
ref={eventsTree => (this._eventsTree = eventsTree)}
key={events.ptr}
indentScale={preferences.values.eventsSheetIndentScale}
onScroll={this._ensureFocused}
events={events}
project={project}

View File

@@ -13,8 +13,9 @@ import {
registerNewPreviewWindow,
} from './BrowserPreviewDebuggerServer';
import Window from '../../../Utils/Window';
import { displayBlackLoadingScreen } from '../../../Utils/BrowserExternalWindowUtils';
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
import { isNativeMobileApp } from '../../../Utils/Platform';
const gd: libGDevelop = global.gd;
type State = {|
@@ -47,8 +48,13 @@ export const immediatelyOpenNewPreviewWindow = (
targetId,
`width=${width},height=${height},left=${left},top=${top}`
);
if (!previewWindow) {
throw new Error(
"Can't open the preview window because of browser restrictions."
);
}
displayBlackLoadingScreen(previewWindow);
displayBlackLoadingScreenOrThrow(previewWindow);
return previewWindow;
};
@@ -117,9 +123,17 @@ export default class BrowserS3PreviewLauncher extends React.Component<
const previewWindows = existingPreviewWindow
? [existingPreviewWindow]
: Array.from({ length: numberOfWindows }, () =>
immediatelyOpenNewPreviewWindow(project)
);
: Array.from({ length: numberOfWindows }, () => {
try {
return immediatelyOpenNewPreviewWindow(project);
} catch (error) {
console.error(
'Unable to open a new preview window - this window will be ignored:',
error
);
return null;
}
}).filter(Boolean);
try {
await this.getPreviewDebuggerServer().startServer();
@@ -159,6 +173,8 @@ export default class BrowserS3PreviewLauncher extends React.Component<
previewOptions.fullLoadingScreen
);
previewExportOptions.setNativeMobileApp(isNativeMobileApp());
if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(
previewOptions.fallbackAuthor.id,

View File

@@ -36,6 +36,8 @@ import Window from '../../Utils/Window';
import Checkbox from '../../UI/Checkbox';
import SemiControlledTextField from '../../UI/SemiControlledTextField';
const LEADERBOARD_APPEARANCE_CUSTOM_CSS_MAX_LENGTH = 20000;
const unitToAbbreviation = {
hour: 'HH',
minute: 'MM',
@@ -422,7 +424,18 @@ function LeaderboardAppearanceDialog({
</Line>
</GetSubscriptionCard>
) : (
<LeaderboardPlaygroundCard />
<>
{customCss.length >
LEADERBOARD_APPEARANCE_CUSTOM_CSS_MAX_LENGTH && (
<AlertMessage kind="error">
<Trans>
Custom css value cannot exceed{' '}
{LEADERBOARD_APPEARANCE_CUSTOM_CSS_MAX_LENGTH} characters.
</Trans>
</AlertMessage>
)}
<LeaderboardPlaygroundCard />
</>
)}
<Spacer />
<Text size="sub-title" noMargin>

View File

@@ -22,7 +22,7 @@ import { duplicateLeaderboard } from '../Utils/GDevelopServices/Play';
import { registerGame } from '../Utils/GDevelopServices/Game';
import { toNewGdMapStringString } from '../Utils/MapStringString';
const gd = global.gd;
const gd: libGDevelop = global.gd;
type ReplacePromptDialogProps = {|
leaderboardsToReplace: ?Array<string>,
@@ -181,7 +181,7 @@ export const LeaderboardReplacerProgressDialog = ({
type RetryOrAbandonCallback = () => void;
type UseLeaderboardReplacerOutput = {
type UseLeaderboardReplacerOutput = {|
/**
* Launch search through the whole project for leaderboard ids to replace.
*/
@@ -191,7 +191,7 @@ type UseLeaderboardReplacerOutput = {
* Render, if needed, the dialog that will show the progress of leaderboard replacement.
*/
renderLeaderboardReplacerDialog: () => React.Node,
};
|};
type ErroredLeaderboard = {
leaderboardId: string,
@@ -260,7 +260,10 @@ export const useLeaderboardReplacer = (): UseLeaderboardReplacerOutput => {
templateSlug: project.getTemplateSlug(),
});
} catch (error) {
console.error('Could not register game: ', error);
console.error(
'Could not register game before leaderboard replacement: ',
error
);
}
setProgress(progressStep);
@@ -319,6 +322,7 @@ export const useLeaderboardReplacer = (): UseLeaderboardReplacerOutput => {
gd.ProjectBrowserHelper.exposeProjectEvents(
project,
// $FlowIgnore - eventsLeaderboardReplacer inherits from ArbitraryEventsWorker
eventsLeaderboardReplacer
);
eventsLeaderboardReplacer.delete();
@@ -373,6 +377,7 @@ export const useLeaderboardReplacer = (): UseLeaderboardReplacerOutput => {
setGameId(sourceGameId);
const leaderboardsLister = new gd.EventsLeaderboardsLister(project);
// $FlowIgnore - leaderboardsLister inherits from ArbitraryEventsWorker
gd.ProjectBrowserHelper.exposeProjectEvents(project, leaderboardsLister);
const leaderboardIds = leaderboardsLister.getLeaderboardIds();
setLeaderboardsToReplace(leaderboardIds.toNewVectorString().toJSArray());

View File

@@ -20,7 +20,8 @@ import {
JOYSTICK_IN_APP_TUTORIAL_ID,
OBJECT_3D_IN_APP_TUTORIAL_ID,
KNIGHT_PLATFORMER_IN_APP_TUTORIAL_ID,
TOP_DOWN_RPG_PIXEL_PERFECT_ID,
TOP_DOWN_RPG_MOVEMENT_ID,
FIRE_A_BULLET,
guidedLessonsIds,
} from '../../../../Utils/GDevelopServices/InAppTutorial';
import MultiplierScore from './Icons/MultiplierScore';
@@ -38,6 +39,7 @@ import Trophy from '../../../../UI/CustomSvgIcons/Trophy';
import Object3D from './Icons/Object3D';
import Platformer from './Icons/Platformer';
import TopDownRPGMovement from './Icons/TopDownRPGMovement';
import FireABullet from './Icons/FireAbullet';
const getColumnsFromWindowSize = (
windowSize: WindowSizeType,
@@ -167,12 +169,19 @@ const GuidedLessons = ({ selectInAppTutorial, lessonsIds }: Props) => {
renderImage: props => <Platformer {...props} />,
},
{
id: TOP_DOWN_RPG_PIXEL_PERFECT_ID,
id: TOP_DOWN_RPG_MOVEMENT_ID,
title: t`Top-Down RPG Pixel Perfect`,
description: t`Learn how to make a character move like in the retro Pokemon games.`,
durationInMinutes: 2,
renderImage: props => <TopDownRPGMovement {...props} />,
},
{
id: FIRE_A_BULLET,
title: t`Fire a Bullet`,
description: t`Learn how to fire bullets with the help of a behavior. Get ready for a Star Wars show.`,
durationInMinutes: 3,
renderImage: props => <FireABullet {...props} />,
},
].filter(item => displayedGuidedLessonsIds.includes(item.id));
return (

View File

@@ -0,0 +1,456 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon
{...props}
width="120"
height="138"
viewBox="0 0 120 138"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.6799 84.2797C21.9531 84.4687 22.3125 84.4032 22.5015 84.13L22.2111 87.9229L30.6625 87.5767L31.5081 83.046C31.2981 83.3495 31.394 83.7299 31.6671 83.9189C31.9403 84.1078 32.2997 84.0423 32.5097 83.7388L34.6931 80.5824C36.1418 78.4882 35.6179 75.6129 33.5237 74.1642C31.4295 72.7156 28.5542 73.2395 27.1055 75.3336L21.4999 83.4372C21.3109 83.7103 21.4068 84.0908 21.6799 84.2797Z"
fill="#F84848"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.8304 102.633C16.7744 104.669 20.7793 103.94 22.8159 100.996L24.8524 98.0517C25.0413 97.7786 24.9968 97.3888 24.7237 97.1999C24.4505 97.0109 24.0398 97.0858 23.8508 97.3589L25.2668 95.3768L16.6473 89.4142L15.2313 91.3963C15.4203 91.1232 15.3758 90.7334 15.1026 90.5445C14.8295 90.3555 14.4187 90.4303 14.2298 90.7035L12.1932 93.6475C10.1567 96.5915 10.9168 100.617 13.8304 102.633Z"
fill="#A19DAE"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M64.3828 113.82C61.1417 115.975 60.3303 111.959 61.198 104.347C60.9248 104.158 60.829 103.778 61.039 103.474L63.2224 100.318C64.6711 98.2235 67.5464 97.6996 69.6406 99.1482C71.7348 100.597 72.289 103.493 70.8404 105.587L65.2347 113.691C65.0458 113.964 64.656 114.009 64.3828 113.82Z"
fill="#F84848"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M49.9473 127.617C47.0337 125.601 46.2736 121.575 48.3101 118.632L50.3466 115.688C50.5356 115.414 50.9463 115.34 51.2195 115.528C51.4926 115.717 51.5675 116.128 51.3785 116.401L53.674 113.278L62.2935 119.24L59.9677 122.343C60.1566 122.07 60.5674 121.995 60.8405 122.184C61.1137 122.373 61.1885 122.784 60.9996 123.057L58.9631 126.001C56.9266 128.945 52.8913 129.653 49.9473 127.617Z"
fill="#A19DAE"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M65.993 119.017L59.3896 99.3715C51.9144 101.29 49.125 106.945 47.183 112.736L28.7603 99.9921C36.0605 98.1315 40.3044 93.6182 36.8464 83.9566L38.0648 83.9468C38.4032 83.9117 38.6738 83.6501 38.6597 83.2814C38.6549 82.9639 38.363 82.6723 38.0246 82.7074L15.8702 83.4019C15.6858 83.409 15.4898 83.4978 15.3848 83.6495L11.7107 88.9608C11.5217 89.234 11.5966 89.6448 11.8697 89.8337L62.6459 124.958C62.919 125.147 63.3298 125.072 63.5187 124.799L67.1929 119.488C67.2978 119.336 67.3118 119.121 67.2533 118.946L60.1733 97.8494C60.0565 97.4993 59.711 97.3501 59.3819 97.4365C59.0832 97.544 58.8826 97.8989 59.0204 98.2186L65.993 119.017Z"
fill="#CDCDCD"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M45.8014 113.306L28.6838 101.465C28.4713 101.318 28.3639 101.019 28.4501 100.765C31.7236 91.3621 37.3404 82.0099 43.8778 75.0893C47.4545 71.2812 51.1482 68.4066 54.9076 66.4749C59.0287 64.3447 63.1082 63.4423 67.0436 63.7862C67.2373 63.8305 67.4194 63.9565 67.5595 64.1432C69.2694 67.7043 69.8638 71.8399 69.324 76.4475C68.8121 80.6257 67.425 85.0959 65.1534 89.8068C60.9834 98.3649 54.2132 106.919 46.5389 113.278C46.3313 113.448 46.0442 113.474 45.8014 113.306Z"
fill="#E4E4E4"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M41.4916 110.325L32.9936 104.446C32.7204 104.257 32.6759 103.868 32.8649 103.594L45.2309 85.7181C47.0575 83.0776 50.6516 82.4227 53.2618 84.2283C55.9022 86.0548 56.5571 89.649 54.7305 92.2895L42.3645 110.166C42.1756 110.439 41.7648 110.514 41.4916 110.325Z"
fill="#B9B9B9"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M69.324 76.4476C69.8638 71.84 69.2694 67.7043 67.5595 64.1432C67.4194 63.9565 67.2373 63.8305 67.0436 63.7863C63.1083 63.4423 59.0287 64.3448 54.9076 66.475C54.4945 66.6828 54.1328 66.8814 53.7198 67.0893C55.9052 69.1844 58.3591 71.1511 61.0299 72.9986C63.7007 74.8462 66.4368 76.4697 69.1378 77.7547C69.217 77.3159 69.2752 76.9074 69.324 76.4476Z"
fill="#F8485E"
/>
<path
d="M81.5 20.5C83 16.5 86.8333 9.83333 89 7.99999C90.5 6.72121 107 7.49998 108.5 7.99998C110 8.49998 113.5 12 115 13.5C116.5 15 116 30 115 31.5C114 33 104.5 43.5 102 43.5C99.5 43.5 82.5 39.5 81.5 37C80.5531 34.6328 80.2586 23.8104 81.5 20.5Z"
fill="#C17E47"
/>
<path
d="M86.5843 21.4791C87.6474 18.6058 90.3645 13.8172 91.9002 12.5003C92.9634 11.5817 104.659 12.1411 105.722 12.5003C106.785 12.8594 109.266 15.3735 110.329 16.4509C111.392 17.5284 111.038 28.3029 110.329 29.3804C109.62 30.4578 102.887 38 101.115 38C99.3426 38 87.293 35.1268 86.5842 33.331C85.9131 31.6307 85.7044 23.8569 86.5843 21.4791Z"
fill="#D89C60"
/>
<ellipse cx="105.5" cy="16.5" rx="2.5" ry="1.5" fill="#C17E47" />
<circle cx="93.5" cy="14.5" r="1.5" fill="#C17E47" />
<circle cx="100.5" cy="35.5" r="1.5" fill="#C17E47" />
<path
d="M22.5365 12.639C24.2255 13.1884 27.0705 14.6704 27.8779 15.5504C28.4408 16.1595 28.4893 23.1838 28.316 23.8318C28.1428 24.4798 26.7731 26.0428 26.1861 26.7127C25.599 27.3825 19.3804 27.4986 18.7372 27.1068C18.094 26.7149 13.5353 22.9103 13.4792 21.8486C13.423 20.7869 14.6963 13.4797 15.7084 13.0002C16.6667 12.5463 21.1387 12.1843 22.5365 12.639Z"
fill="#C17E47"
/>
<path
d="M21.8353 15.7177C22.8067 16.0337 24.4431 16.8861 24.9074 17.3923C25.2312 17.7426 25.2591 21.7827 25.1595 22.1554C25.0598 22.5281 24.272 23.4271 23.9344 23.8124C23.5967 24.1977 20.0199 24.2645 19.65 24.0391C19.28 23.8137 16.6581 21.6254 16.6257 21.0148C16.5934 20.4041 17.3258 16.2012 17.9079 15.9255C18.4591 15.6644 21.0313 15.4562 21.8353 15.7177Z"
fill="#D89C60"
/>
<path
d="M99.3573 121.809C100.161 120.82 101.887 119.297 102.685 118.984C103.238 118.764 107.954 120.543 108.343 120.827C108.732 121.11 109.426 122.43 109.723 122.996C110.02 123.562 108.493 127.756 108.065 128.086C107.637 128.415 103.913 130.486 103.188 130.25C102.462 130.014 97.8977 127.276 97.8378 126.475C97.781 125.716 98.6922 122.628 99.3573 121.809Z"
fill="#C17E47"
/>
<path
d="M101.238 123.073C101.7 122.504 102.693 121.628 103.152 121.448C103.47 121.321 106.183 122.345 106.406 122.508C106.63 122.671 107.029 123.43 107.2 123.756L107.2 123.756C107.371 124.081 106.493 126.494 106.246 126.683C106 126.873 103.858 128.064 103.441 127.928C103.024 127.792 100.398 126.217 100.364 125.757C100.331 125.32 100.855 123.544 101.238 123.073Z"
fill="#D89C60"
/>
<g filter="url(#filter0_dd_153_2)">
<path
d="M74.4037 93.3096L78.7656 85.9839"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<g filter="url(#filter1_dd_153_2)">
<path
d="M38 68.7202L42.8935 61.7383"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<g filter="url(#filter2_dd_153_2)">
<path
d="M81.819 80.856L86.181 73.5303"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<g filter="url(#filter3_dd_153_2)">
<path
d="M46.3189 56.851L51.2124 49.8691"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<g filter="url(#filter4_dd_153_2)">
<path
d="M89.2344 68.4023L93.5963 61.0766"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<g filter="url(#filter5_dd_153_2)">
<path
d="M54.6378 44.9819L59.5313 38"
stroke="#00F0FF"
strokeWidth="2"
strokeLinecap="round"
/>
</g>
<defs>
<filter
id="filter0_dd_153_2"
x="68.379"
y="80.0089"
width="16.4113"
height="19.2759"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
<filter
id="filter1_dd_153_2"
x="31.9734"
y="55.7654"
width="16.9466"
height="18.9277"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
<filter
id="filter2_dd_153_2"
x="75.7944"
y="67.5552"
width="16.4113"
height="19.2759"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
<filter
id="filter3_dd_153_2"
x="40.2924"
y="43.8962"
width="16.9466"
height="18.9277"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
<filter
id="filter4_dd_153_2"
x="83.2097"
y="55.1016"
width="16.4113"
height="19.2759"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
<filter
id="filter5_dd_153_2"
x="48.6113"
y="32.0271"
width="16.9466"
height="18.9277"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.76 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_153_2"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 1 0 0 0 0 0.4 0 0 0 1 0"
/>
<feBlend
mode="normal"
in2="effect1_dropShadow_153_2"
result="effect2_dropShadow_153_2"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect2_dropShadow_153_2"
result="shape"
/>
</filter>
</defs>
</SvgIcon>
));

View File

@@ -194,6 +194,7 @@ export type PreferencesValues = {|
autosaveOnPreview: boolean,
useGDJSDevelopmentWatcher: boolean,
eventsSheetUseAssignmentOperators: boolean,
eventsSheetIndentScale: number,
eventsSheetZoomLevel: number,
showEffectParameterNames: boolean,
projectLastUsedPaths: { [string]: { [ResourceKind]: string } },
@@ -250,6 +251,7 @@ export type Preferences = {|
setAutosaveOnPreview: (enabled: boolean) => void,
setUseGDJSDevelopmentWatcher: (enabled: boolean) => void,
setEventsSheetUseAssignmentOperators: (enabled: boolean) => void,
setEventsSheetIndentScale: (scale: number) => void,
setEventsSheetZoomLevel: (zoomLevel: number) => void,
setShowEffectParameterNames: (enabled: boolean) => void,
getLastUsedPath: (project: gdProject, kind: ResourceKind) => string,
@@ -345,6 +347,7 @@ export const initialPreferences = {
useGDJSDevelopmentWatcher: true,
eventsSheetUseAssignmentOperators: false,
eventsSheetZoomLevel: 14,
eventsSheetIndentScale: 1,
showEffectParameterNames: false,
projectLastUsedPaths: {},
defaultEditorMosaicNodes: {},
@@ -392,6 +395,7 @@ export const initialPreferences = {
setAutosaveOnPreview: () => {},
setUseGDJSDevelopmentWatcher: (enabled: boolean) => {},
setEventsSheetUseAssignmentOperators: (enabled: boolean) => {},
setEventsSheetIndentScale: (scale: number) => {},
setEventsSheetZoomLevel: (zoomLevel: number) => {},
setShowEffectParameterNames: (enabled: boolean) => {},
getLastUsedPath: (project: gdProject, kind: ResourceKind) => '',

View File

@@ -54,6 +54,7 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
setAutosaveOnPreview,
setUseGDJSDevelopmentWatcher,
setEventsSheetUseAssignmentOperators,
setEventsSheetIndentScale,
getDefaultEditorMosaicNode,
setDefaultEditorMosaicNode,
setAutoOpenMostRecentProject,
@@ -289,6 +290,23 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
labelPosition="right"
label={<Trans>Display assignment operators in Events Sheets</Trans>}
/>
<SelectField
floatingLabelText={<Trans>Indent Scale in Events Sheet</Trans>}
fullWidth
value={values.eventsSheetIndentScale.toString()}
onChange={(e, i, value) => {
setEventsSheetIndentScale(parseInt(value, 10));
}}
>
<SelectOption value="1" label={t`100% (Default)`} />
<SelectOption value="2" label={t`200%`} />
<SelectOption value="3" label={t`300%`} />
<SelectOption value="4" label={t`400%`} />
<SelectOption value="5" label={t`500%`} />
<SelectOption value="6" label={t`600%`} />
<SelectOption value="7" label={t`700%`} />
<SelectOption value="8" label={t`800%`} />
</SelectField>
<SelectField
floatingLabelText={
<Trans>

View File

@@ -115,6 +115,7 @@ export default class PreferencesProvider extends React.Component<Props, State> {
setEventsSheetUseAssignmentOperators: this._setEventsSheetUseAssignmentOperators.bind(
this
),
setEventsSheetIndentScale: this._setEventsSheetIndentScale.bind(this),
setEventsSheetZoomLevel: this._setEventsSheetZoomLevel.bind(this),
setShowEffectParameterNames: this._setShowEffectParameterNames.bind(this),
getLastUsedPath: this._getLastUsedPath.bind(this),
@@ -310,6 +311,18 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_setEventsSheetIndentScale(eventsSheetIndentScale: number) {
this.setState(
state => ({
values: {
...state.values,
eventsSheetIndentScale,
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_setEventsSheetZoomLevel(eventsSheetZoomLevel: number) {
this.setState(
state => ({

View File

@@ -0,0 +1,75 @@
// @flow
import * as React from 'react';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { duplicateLobbyConfiguration } from '../Utils/GDevelopServices/Play';
import { registerGame } from '../Utils/GDevelopServices/Game';
const gd: libGDevelop = global.gd;
type UseMultiplayerLobbyConfiguratorOutput = {|
configureMultiplayerLobbiesIfNeeded: (
project: gdProject,
sourceGameId: string
) => Promise<void>,
|};
/**
* Hook allowing to duplicate lobby configuration from another project, useful after
* opening a project from an example.
*/
export const useMultiplayerLobbyConfigurator = (): UseMultiplayerLobbyConfiguratorOutput => {
const { profile, getAuthorizationHeader } = React.useContext(
AuthenticatedUserContext
);
const configureMultiplayerLobbiesIfNeeded = React.useCallback(
async (project: gdProject, sourceGameId: string) => {
const isMultiplayerExtensionUsed = gd.UsedExtensionsFinder.scanProject(
project
)
.getUsedExtensions()
.toNewVectorString()
.toJSArray()
.some(extensionName => extensionName === 'Multiplayer');
if (!isMultiplayerExtensionUsed) return;
if (!profile) {
console.warn(
'User is not connected. Aborting multiplayer lobby configuration.'
);
return;
}
try {
// Register game. The error will silently be caught if the game already exists.
try {
await registerGame(getAuthorizationHeader, profile.id, {
gameId: project.getProjectUuid(),
authorName: project.getAuthor() || 'Unspecified publisher',
gameName: project.getName() || 'Untitled game',
templateSlug: project.getTemplateSlug(),
});
} catch (error) {
console.error(
'Could not register game before lobby configuration: ',
error
);
}
await duplicateLobbyConfiguration({
userId: profile.id,
getAuthorizationHeader,
gameId: project.getProjectUuid(),
sourceGameId,
});
} catch (error) {
console.error(
`An error occurred while copying lobby configuration from game with id ${sourceGameId}. Failing silently: `,
error
);
}
},
[getAuthorizationHeader, profile]
);
return {
configureMultiplayerLobbiesIfNeeded,
};
};

View File

@@ -142,7 +142,7 @@ import {
sendInAppTutorialStarted,
sendEventsExtractedAsFunction,
} from '../Utils/Analytics/EventSender';
import { useLeaderboardReplacer } from '../Leaderboard/useLeaderboardReplacer';
import { useLeaderboardReplacer } from '../Leaderboard/UseLeaderboardReplacer';
import useAlertDialog from '../UI/Alert/useAlertDialog';
import NewProjectSetupDialog from '../ProjectCreation/NewProjectSetupDialog';
import {
@@ -178,6 +178,7 @@ import useVersionHistory from '../VersionHistory/UseVersionHistory';
import { ProjectManagerDrawer } from '../ProjectManager/ProjectManagerDrawer';
import DiagnosticReportDialog from '../ExportAndShare/DiagnosticReportDialog';
import useSaveReminder from './UseSaveReminder';
import { useMultiplayerLobbyConfigurator } from './UseMultiplayerLobbyConfigurator';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
@@ -410,6 +411,9 @@ const MainFrame = (props: Props) => {
findLeaderboardsToReplace,
renderLeaderboardReplacerDialog,
} = useLeaderboardReplacer();
const {
configureMultiplayerLobbiesIfNeeded,
} = useMultiplayerLobbyConfigurator();
const eventsFunctionsExtensionsState = React.useContext(
EventsFunctionsExtensionsContext
);
@@ -1148,6 +1152,7 @@ const MainFrame = (props: Props) => {
setNewProjectSetupDialogOpen(false);
closeExampleStoreDialog({ deselectExampleAndGameTemplate: true });
findLeaderboardsToReplace(project, oldProjectId);
configureMultiplayerLobbiesIfNeeded(project, oldProjectId);
openSceneOrProjectManager({
currentProject: project,
editorTabs: editorTabs,

View File

@@ -30,6 +30,7 @@ import PixiResourcesLoader, {
import useAlertDialog from '../../UI/Alert/useAlertDialog';
import { PropertyResourceSelector, PropertyField } from './PropertyFields';
import AlertMessage from '../../UI/AlertMessage';
import Window from '../../Utils/Window';
const gd: libGDevelop = global.gd;
@@ -48,6 +49,9 @@ const styles = {
flex: 1,
alignItems: 'center',
},
neverShrinkingButton: {
flexShrink: 0,
},
};
const SpineEditor = ({
@@ -307,9 +311,22 @@ const SpineEditor = ({
<ScrollView ref={scrollView}>
<ColumnStackLayout noMargin>
{renderObjectNameField && renderObjectNameField()}
<AlertMessage kind="warning">
<AlertMessage
kind="warning"
renderRightButton={() => (
<FlatButton
style={styles.neverShrinkingButton}
label={<Trans>Purchase Spine</Trans>}
onClick={() =>
Window.openExternalURL(
'https://esotericsoftware.com/spine-purchase'
)
}
/>
)}
>
<Trans>
You need to own a license of Spine to publish a game with a Spine
You must own a Spine license to publish a game with a Spine
object.
</Trans>
</AlertMessage>

View File

@@ -328,7 +328,7 @@ export const LoadingScreenEditor = ({
const currentProgressBarMinWidth = loadingScreen.getProgressBarMinWidth();
const newProgressBarMinWidth = Math.max(
0,
parseFloat(newValue)
parseFloat(newValue) || 0
);
if (currentProgressBarMinWidth === newProgressBarMinWidth) {
return;
@@ -347,7 +347,7 @@ export const LoadingScreenEditor = ({
const currentProgressBarWidthPercent = loadingScreen.getProgressBarWidthPercent();
const newProgressBarWidthPercent = Math.min(
100,
Math.max(1, parseFloat(newValue))
Math.max(1, parseFloat(newValue) || 0)
);
if (
currentProgressBarWidthPercent === newProgressBarWidthPercent
@@ -371,7 +371,7 @@ export const LoadingScreenEditor = ({
const currentProgressBarMaxWidth = loadingScreen.getProgressBarMaxWidth();
const newProgressBarMaxWidth = Math.max(
0,
parseFloat(newValue)
parseFloat(newValue) || 0
);
if (currentProgressBarMaxWidth === newProgressBarMaxWidth) {
return;
@@ -390,7 +390,10 @@ export const LoadingScreenEditor = ({
value={'' + loadingScreen.getProgressBarHeight()}
onChange={newValue => {
const currentProgressBarHeight = loadingScreen.getProgressBarHeight();
const newProgressBarHeight = Math.max(1, parseFloat(newValue));
const newProgressBarHeight = Math.max(
1,
parseFloat(newValue) || 0
);
if (currentProgressBarHeight === newProgressBarHeight) {
return;
}
@@ -427,7 +430,7 @@ export const LoadingScreenEditor = ({
type="number"
value={'' + loadingScreen.getMinDuration()}
onChange={newValue => {
const newMinDuration = Math.max(0, parseFloat(newValue));
const newMinDuration = Math.max(0, parseFloat(newValue) || 0);
if (
newMinDuration < forcedLogo.minDuration &&
!watermark.isGDevelopWatermarkShown() &&
@@ -466,7 +469,7 @@ export const LoadingScreenEditor = ({
onChange={newValue => {
const newLogoAndProgressLogoFadeInDelay = Math.max(
0,
parseFloat(newValue)
parseFloat(newValue) || 0
);
if (
newLogoAndProgressLogoFadeInDelay >
@@ -507,7 +510,7 @@ export const LoadingScreenEditor = ({
onChange={newValue => {
const newLogoAndProgressFadeInDuration = Math.max(
0,
parseFloat(newValue)
parseFloat(newValue) || 0
);
if (
newLogoAndProgressFadeInDuration >

View File

@@ -491,7 +491,7 @@ const ProjectPropertiesDialog = (props: Props) => {
onChange={value => {
const newResolutionWidth = Math.max(
1,
parseInt(value, 10)
parseInt(value, 10) || 0
);
if (newResolutionWidth === gameResolutionWidth) {
return;
@@ -509,7 +509,7 @@ const ProjectPropertiesDialog = (props: Props) => {
onChange={value => {
const newResolutionHeight = Math.max(
1,
parseInt(value, 10)
parseInt(value, 10) || 0
);
if (newResolutionHeight === gameResolutionHeight) {
return;
@@ -570,7 +570,7 @@ const ProjectPropertiesDialog = (props: Props) => {
type="number"
value={'' + minFPS}
onChange={value => {
const newMinFPS = Math.max(0, parseInt(value, 10));
const newMinFPS = Math.max(0, parseInt(value, 10) || 0);
if (newMinFPS === minFPS) {
return;
}
@@ -586,7 +586,7 @@ const ProjectPropertiesDialog = (props: Props) => {
type="number"
value={'' + maxFPS}
onChange={value => {
const newMaxFPS = Math.max(0, parseInt(value, 10));
const newMaxFPS = Math.max(0, parseInt(value, 10) || 0);
if (newMaxFPS === maxFPS) {
return;
}

View File

@@ -999,7 +999,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
globalVariablesItemId,
i18n._(t`Global variables`),
openProjectVariables,
'res/icons_default/publish_black.svg'
'res/icons_default/global_variable24_black.svg'
)
),
new LeafTreeViewItem(

View File

@@ -20,7 +20,7 @@ import {
type ItemResult,
} from '../Utils/BlobDownloader';
import { showWarningBox } from '../UI/Messages/MessageBox';
import { displayBlackLoadingScreen } from '../Utils/BrowserExternalWindowUtils';
import { displayBlackLoadingScreenOrThrow } from '../Utils/BrowserExternalWindowUtils';
import { UserCancellationError } from '../LoginProvider/Utils';
let nextExternalEditorWindowId = 0;
@@ -350,7 +350,7 @@ const immediatelyOpenLoadingWindowForExternalEditor = () => {
);
}
displayBlackLoadingScreen(externalEditorWindow);
displayBlackLoadingScreenOrThrow(externalEditorWindow);
return externalEditorWindow;
};

View File

@@ -25,6 +25,7 @@ import { Accordion, AccordionHeader, AccordionBody } from '../UI/Accordion';
import { IconContainer } from '../UI/IconContainer';
import { getBehaviorTutorialIds } from '../Utils/GDevelopServices/Tutorial';
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import SceneVariable from '../UI/CustomSvgIcons/SceneVariable';
const gd: libGDevelop = global.gd;
@@ -212,6 +213,7 @@ const ScenePropertiesDialog = ({
secondaryActions={[
<RaisedButton
key="edit-scene-variables"
icon={<SceneVariable fontSize="small" />}
label={<Trans>Edit scene variables</Trans>}
fullWidth
onClick={() => {

View File

@@ -12,23 +12,19 @@ import { type EditorId } from '..';
import Paper from '../../UI/Paper';
const iconSize = 24;
const iconButtonPaddingTop = 8;
/**
* Padding bottom is bigger than padding top to leave space for the Android/iOS
* Padding bottom is added to toolbar to leave space for the Android/iOS
* bottom navigation bar.
*/
const iconButtonPaddingBottom = 20;
const iconButtonPaddingHorizontal = 8;
const toolbarPaddingBottom = 12;
const iconButtonPadding = 8;
const iconButtonLabelPadding = 2;
const toolbarHeight =
iconSize +
2 * iconButtonLabelPadding +
iconButtonPaddingTop +
iconButtonPaddingBottom;
iconSize + 2 * iconButtonLabelPadding + 2 * iconButtonPadding;
const styles = {
iconButton: {
padding: `${iconButtonPaddingTop}px ${iconButtonPaddingHorizontal}px ${iconButtonPaddingBottom}px ${iconButtonPaddingHorizontal}px`,
padding: iconButtonPadding,
fontSize: 'inherit',
},
buttonLabel: {
@@ -69,7 +65,7 @@ const editors = {
const BottomToolbar = React.memo<Props>((props: Props) => {
return (
<Paper background="medium" square style={styles.container}>
<Toolbar height={toolbarHeight}>
<Toolbar height={toolbarHeight} paddingBottom={toolbarPaddingBottom}>
<ToolbarGroup>
{Object.keys(editors).map(editorId => {
const { icon, buttonId } = editors[editorId];

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