Compare commits

..

397 Commits

Author SHA1 Message Date
Davy Hélard
62ed7a4998 Put an icon on empty shape painter instances 2025-10-11 15:51:09 +02:00
Davy Hélard
dc5dab7086 Fix particle helper destruction 2025-10-11 15:48:43 +02:00
Davy Hélard
dad7546099 Display the 2D particle emitter renderer 2025-10-09 18:49:56 +02:00
Davy Hélard
0a55eb46d3 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-10-09 17:28:15 +02:00
Davy Hélard
2678d86783 Forbid selection when transform controls are used 2025-10-09 17:03:43 +02:00
Davy Hélard
b31618568a Fix default grid depth. 2025-10-09 16:16:10 +02:00
Davy Hélard
b3b4681c51 Fix grid not following the object 2025-10-09 15:33:42 +02:00
Davy Hélard
b1b7643efc Fix rotation controls 2025-10-09 14:58:07 +02:00
Davy Hélard
d43479cbe5 Avoid too big angles 2025-10-09 14:15:53 +02:00
Florian Rival
76d43e1695 Fix instances not visible when added by the AI 2025-10-05 22:43:16 +02:00
Davy Hélard
c3ee9d9891 Display the grid for duplicated and dragged objects 2025-10-03 17:45:26 +02:00
Davy Hélard
313f7857e2 Use the yellow point instead of Alt to drag the selection 2025-10-03 16:26:10 +02:00
Davy Hélard
3ac2aec229 Show the grid when dragging a new instance 2025-10-03 14:11:08 +02:00
Davy Hélard
92e8dfdf04 Keep the camera angle when centering on an object 2025-10-03 10:43:31 +02:00
Davy Hélard
80726da56d Add a getter for areEffectsHiddenInEditor. 2025-10-03 10:39:45 +02:00
Davy Hélard
3098fb034f Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-10-02 14:16:49 +02:00
Davy Hélard
9d0f172663 Format 2025-10-02 13:25:36 +02:00
Davy Hélard
e0d2f16dc1 Fix aspect ratio being lost when hot-reloading 3D models 2025-10-02 11:31:22 +02:00
Davy Hélard
0578c45b0f Additional rounding of object locations 2025-10-01 16:35:28 +02:00
Davy Hélard
a8866a7bd6 Use the origin point for translation and scaling 2025-10-01 11:49:16 +02:00
Davy Hélard
ec62e33530 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-30 18:10:57 +02:00
Davy Hélard
e1f12837f1 Fix hot-reloading not doing anything 2025-09-30 16:28:44 +02:00
Davy Hélard
732f3c14b9 Add a missing variant list in tests 2025-09-30 14:44:01 +02:00
Davy Hélard
7fdad45192 Add missing InstancesEditorSettings in tests 2025-09-30 14:31:46 +02:00
Davy Hélard
94590d3713 Fix to avoid to hot-reload the editor with game and the other way around 2025-09-30 13:04:13 +02:00
Davy Hélard
de310512a3 Fix captures not being done at preview 2025-09-30 13:04:13 +02:00
Florian Rival
a9edd2f21a Ensure Box2D wasm is properly loaded whatever the environment/protocol (#7862)
Tested on: 
- [x] preview (web-app, cloud project)
- [x] in-game editor (web-app, cloud project)
- [x] preview (electron cloud project, electron local project)
- [x] in-game editor (electron cloud project, electron local project)
- [x] cordova android
- [x] cordova ios
- [x] electron export (windows, macos)
2025-09-30 12:16:45 +02:00
Davy Hélard
0cc03bd4b5 Fix hot-reload losing model aspect ratio 2025-09-29 19:25:42 +02:00
Florian Rival
cdbcaf59d9 Fix framerate when dragging a new object 2025-09-29 17:10:36 +02:00
Florian Rival
4e9b09e426 Fix border color with AI pane 2025-09-29 17:08:03 +02:00
Davy Hélard
3d843a0170 Fix grid opacity 2025-09-29 14:10:24 +02:00
Davy Hélard
ee2f7fb8dc Fix the grid to snap on the object origin 2025-09-29 13:58:34 +02:00
Davy Hélard
a780601230 Fix custom object selection 2025-09-29 11:21:02 +02:00
Florian Rival
cc42923e16 Add support for touch controls in editor 2025-09-28 19:21:06 +02:00
Davy Hélard
0e8a223b24 Fix controls being put at (0; 0; 0) when switching of scene and a custom object is selected 2025-09-28 19:00:21 +02:00
Florian Rival
c2f17f9348 Improve rendering of the 2D plane in 3D so it's not cut when rotating the camera (#7857)
Allow the 2D plane in the 3D world to be rendered at the position "where
the 3D camera is looking at" (by projecting the camera frustrum on the Z
plane, with a "maximum drawing distance" to avoid a too large area). The
2D rendering texture size is not changed, so an area too big can create
blurry rendering near the camera - but this is expected.

In practice, 2D objects culling will limit the distance anyway of what's
visible.
2025-09-27 15:33:01 +02:00
Davy Hélard
0ba59d5d7f Remove log 2025-09-26 17:48:26 +02:00
Davy Hélard
6878a4cd75 Handle grid in variant tabs 2025-09-26 17:45:09 +02:00
Davy Hélard
33eed58c62 Save editor settings for variant tabs 2025-09-26 17:45:08 +02:00
Florian Rival
f516eff739 Fix toolbar not updating when enabling grid 2025-09-26 17:39:09 +02:00
Florian Rival
baefc272f6 Remove some TODOs and fix enabling/disabling grid 2025-09-26 17:25:51 +02:00
Florian Rival
2badc72dfb Remove useless fields 2025-09-26 16:44:10 +02:00
Davy Hélard
5e54f02061 Handle grid setting changes 2025-09-26 16:37:40 +02:00
Davy Hélard
827bb2d08d Fix grid axe when Alt is pressed 2025-09-25 17:30:28 +02:00
Davy Hélard
0fda0baa48 Remove log 2025-09-25 17:00:45 +02:00
Davy Hélard
e655cc0661 Fix undefined cell depth 2025-09-25 16:53:27 +02:00
Davy Hélard
225d1b37ab Remove log 2025-09-25 16:27:22 +02:00
Davy Hélard
5a0ab9dffa Snap objects on the grid (no grid setting update) 2025-09-25 16:08:23 +02:00
Davy Hélard
9300604d56 Round angles 2025-09-24 17:18:58 +02:00
Davy Hélard
02fcf1dbc2 Fix scaling control rotation 2025-09-24 17:04:25 +02:00
Florian Rival
a70b2e2c2c Fix low framerate when dragging a new instance 2025-09-24 12:55:59 +02:00
Davy Hélard
3e04b5a82f Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-24 10:36:54 +02:00
Florian Rival
cbcf0b7b70 Fix undo/redo stacking empty changes when selection is not even moved 2025-09-23 18:39:49 +02:00
Florian Rival
5d1fe83655 Fix undo/redo for default values of instances 2025-09-23 18:01:17 +02:00
Florian Rival
7cf70a6fd7 Reverse Q/E 2025-09-23 00:46:10 +02:00
Florian Rival
24c2dbc340 Add some tolerance radius when selecting an object 2025-09-22 23:26:31 +02:00
Florian Rival
290901849c Fix in-game edition framerate limitation 2025-09-22 17:47:51 +02:00
Florian Rival
59721cba7e Reduce scale speed 2025-09-22 13:08:30 +02:00
Florian Rival
53cd78bb45 Allow to scroll directly with mouse wheel click (even if shift not pressed) 2025-09-22 12:39:52 +02:00
Florian Rival
49f8ce9385 Remove default shortcut S to open scene properties to avoid conflict when using the new editor 2025-09-22 12:26:05 +02:00
Florian Rival
de2a9725f9 Adapt framerate of in-game editor according to usage/visibility 2025-09-21 22:29:14 +02:00
Florian Rival
b2ec3b5387 Merge branch 'master' into experimental-build/move-instance-in-game-2 2025-09-21 15:18:17 +02:00
Florian Rival
1873b5e592 Fix useless methods 2025-09-20 15:59:06 +02:00
Florian Rival
e47e35c090 Add shift + mouse wheel to pan 2025-09-20 15:45:23 +02:00
Florian Rival
ae572683f1 Add free camera speed boost with Shift 2025-09-20 12:14:44 +02:00
Florian Rival
f8e387230f Add ESC to clear selection, default to free camera, show a grab cursor when space pressed to pan (even if in orbit) 2025-09-20 12:09:52 +02:00
Florian Rival
9451e5969f Fix moving on the plane orthogonal to the camera with arrows/mouse wheel or Q/E 2025-09-19 16:51:36 +02:00
Florian Rival
ad7ddf09a3 Fix warning 2025-09-19 14:04:36 +02:00
Davy Hélard
8e70930d8d Fix default light not being exported 2025-09-18 17:15:42 +02:00
Davy Hélard
ab3bda24dc Fix a crash when undoing and a custom object is in the scene 2025-09-18 13:29:32 +02:00
Florian Rival
d33a7331b3 Fix custom object extraction on web-app 2025-09-18 12:19:12 +02:00
Davy Hélard
4d3793815f Fix a crash when adding a layer 2025-09-18 11:40:28 +02:00
Florian Rival
e1835d1144 Fix orbit camera restore 2025-09-17 19:16:08 +02:00
Florian Rival
72068460e1 Fix free camera in-game editor restore 2025-09-17 19:11:35 +02:00
Florian Rival
5da76ae655 Try to fix camera restore on in-game edition startup 2025-09-17 18:59:42 +02:00
Florian Rival
b5b25ad710 Fix editor shortcuts on macOS 2025-09-17 18:28:04 +02:00
Florian Rival
2279f069af Only show transform controls on non sealed/locked objects and adapt selection color to make it more obvious when a locked object is selected 2025-09-17 16:50:40 +02:00
Davy Hélard
f0a68db0d4 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-12 16:17:17 +02:00
Davy Hélard
15ed28ad8d Fix a crash at hot-reload after adding a scene 2025-09-12 16:17:10 +02:00
Davy Hélard
4291d5597a Update some todo 2025-09-10 16:26:37 +02:00
Davy Hélard
7af3fa5f2f Fix warning happening on messages which are not answers 2025-09-10 12:24:34 +02:00
Davy Hélard
5ce9591f68 Avoid a useless code generation when extracting a custom object 2025-09-09 18:45:21 +02:00
Davy Hélard
d5929010a7 Fix flow 2025-09-09 18:44:11 +02:00
Davy Hélard
30f2f5256b Fix custom object extraction behavior check 2025-09-09 14:21:20 +02:00
Davy Hélard
806d59fb88 Ask the selection AABB to all debuggers 2025-09-09 14:20:35 +02:00
Davy Hélard
e5f18ae2d8 Factorize the children configuration override check to ensure the hot-reload is consistent with the runtime 2025-09-09 11:55:24 +02:00
Davy Hélard
15a7fd1f85 Force the fov to 45° in the editor 2025-09-09 11:30:49 +02:00
Davy Hélard
3678a0dd45 Remove todo because this dialog should rarely happen and it now reload the editor instead of the game 2025-09-09 11:14:50 +02:00
Davy Hélard
197bd913b8 Explain why the new position is ignored when an object is dropped 2025-09-08 16:35:39 +02:00
Davy Hélard
baef911d61 Add type declaration 2025-09-08 13:43:49 +02:00
Davy Hélard
771e16e779 Refer to scene and external layout by name to launch previews 2025-09-08 12:11:08 +02:00
Davy Hélard
a2cf8e694b Factorize dragNewInstance 2025-09-08 11:55:18 +02:00
Davy Hélard
cb151ea30e Factorize _removeSelectionControls 2025-09-08 11:37:46 +02:00
Davy Hélard
7d1ebc8963 Fix screen adaptation 2025-09-05 17:55:07 +02:00
Davy Hélard
510a699c03 Remove useless todo 2025-09-05 17:05:09 +02:00
Davy Hélard
08892b68d4 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-04 19:43:52 +02:00
Davy Hélard
e079fa4108 Forbid to extract objects with Physics behaviors 2025-09-04 18:51:05 +02:00
Florian Rival
5a26e883b8 Make text to drop objects more visible on small screens 2025-09-04 18:36:14 +02:00
Florian Rival
6bf5b389b5 Make the embedded game frame works on small screen 2025-09-04 18:33:29 +02:00
Davy Hélard
2837a2906a Forbid to add Physics behaviors on child objects 2025-09-04 15:03:05 +02:00
Davy Hélard
8c63fae2f2 Fix flow 2025-09-04 14:37:19 +02:00
Davy Hélard
f837c22290 Display a message when trying to drag a 2D object 2025-09-04 12:21:48 +02:00
Davy Hélard
465bfa4deb Rename to notifyChangesToInGameEditor 2025-09-04 11:16:39 +02:00
Davy Hélard
a047ecdf9c Show a pop-up when the InGameEditor crashes 2025-09-03 18:31:31 +02:00
Davy Hélard
04c28de00b Hide the object icon when dragging a new object in the 3D editor 2025-09-03 11:52:23 +02:00
Davy Hélard
a7df6de044 Forbid to add 2D objects. 2025-09-02 19:46:57 +02:00
Davy Hélard
81c3199d00 Avoid hot-reloads when the extension doesn't have any custom object 2025-09-02 18:12:26 +02:00
Davy Hélard
dbe93a4bfd Hot-reload when changes are done in extensions 2025-09-02 17:48:34 +02:00
Davy Hélard
e0edbe8d57 Forbid interactions with text inputs from the editor 2025-09-02 11:41:07 +02:00
Davy Hélard
c25d51805f Forbid custom objects to use services from the editor 2025-09-02 11:21:03 +02:00
Davy Hélard
ab695370bb Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-01 18:35:05 +02:00
Davy Hélard
15ee216e33 Fix camera distance not being restored when changing tabs 2025-09-01 18:34:45 +02:00
Davy Hélard
3fac0522c9 Fix a crash when switching of scene 2025-09-01 18:05:10 +02:00
Florian Rival
8d502d7c5c Make it work on the web-app 2025-09-01 15:40:46 +02:00
Davy Hélard
12d18c45bc Rename variables 2025-09-01 13:56:40 +02:00
Davy Hélard
d255ab458b Fix tab switching when extracting a custom object 2025-09-01 12:26:42 +02:00
Florian Rival
fbfa0315de Merge branch 'master' into move-instance-in-game-2 2025-09-01 00:51:10 +02:00
Davy Hélard
9575705d29 Fix the inner area calculus when extracting a custom object 2025-08-29 19:49:27 +02:00
Davy Hélard
1223eaa348 Fix a crash in 2D projects 2025-08-29 12:14:52 +02:00
Davy Hélard
21ea077768 Better default zoom for variants 2025-08-29 11:14:18 +02:00
Davy Hélard
11895decd9 Fix switching to a variant was switching on the previous scene instead 2025-08-29 11:08:53 +02:00
Davy Hélard
c0e1e9fac6 Never generate scene event code for the editor 2025-08-28 19:54:15 +02:00
Davy Hélard
bd9c631e1b Fix inner area not displaying 2025-08-28 19:24:31 +02:00
Davy Hélard
4a6e8ef664 Fix missing included files to edit variants 2025-08-28 19:03:37 +02:00
Davy Hélard
e25345000d Rename to make it clear that the generation is for scenes events 2025-08-28 13:20:31 +02:00
Davy Hélard
111d37fc15 Make the difference between edited layers and camera layers easier to understand 2025-08-27 17:54:25 +02:00
Davy Hélard
4a83c9eb59 Fix inner area no longer displaying 2025-08-27 15:53:51 +02:00
Davy Hélard
177cb2c519 Move createSceneWithCustomObject in the editor 2025-08-27 15:08:32 +02:00
Davy Hélard
b5d69dee4c Clarify a comment 2025-08-27 14:50:43 +02:00
Davy Hélard
fbdaebe575 Avoid to switch the scene 2 times when changing of tab 2025-08-27 13:31:00 +02:00
Davy Hélard
2a2fd75ca3 Fix selection loss at hot-reload 2025-08-27 11:46:17 +02:00
Davy Hélard
af7563b4b7 Fix advanced properties not being hot-reloaded 2025-08-27 09:57:23 +02:00
Davy Hélard
1861c3be41 Handle missing zoom actions 2025-08-26 15:49:51 +02:00
Davy Hélard
895dc625eb Display the screen size rectangle 2025-08-26 11:48:05 +02:00
Davy Hélard
52aea76677 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-08-26 11:07:40 +02:00
Davy Hélard
4f87191176 Fix runtime crash report for in-game editor 2025-08-26 11:02:49 +02:00
Davy Hélard
43d4e2e8cc Force 2D+3D rendering mode. 2025-08-26 10:38:09 +02:00
Davy Hélard
cbfaa13978 Remove log 2025-08-25 17:39:11 +02:00
Davy Hélard
0aba5cf551 Delay the display of loader modals 2025-08-25 17:30:48 +02:00
Davy Hélard
a3ef07c163 Fix editor frame adaptation 2025-08-25 16:44:19 +02:00
Davy Hélard
a8ede5eee7 Fix generated events reload 2025-08-25 16:37:52 +02:00
Davy Hélard
ce965ca31c Fix libraries reload 2025-08-25 16:23:41 +02:00
Davy Hélard
039fbb8b1b Allow to launch back the editor preview after an error 2025-08-25 14:25:28 +02:00
Davy Hélard
80c1e67146 Refactor InGameEditor to no longer rely on SceneStack 2025-08-22 18:46:07 +02:00
Davy Hélard
2591cabdd0 Remove dead code 2025-08-22 16:28:48 +02:00
Davy Hélard
45620d6bf4 Avoid to return a string value from a static function. 2025-08-22 14:41:55 +02:00
Davy Hélard
89a59cdd35 Make a parameter const 2025-08-22 14:20:52 +02:00
Davy Hélard
17b819a423 Avoid to copy strings 2025-08-22 14:12:43 +02:00
Davy Hélard
afb4e3c1c6 Optimize DataProject serialization. 2025-08-22 11:59:56 +02:00
Davy Hélard
e5f8fe1bf8 Make the in-game editor resources from the project folder 2025-08-21 19:11:41 +02:00
Davy Hélard
30de5c6fa9 Use a different folder for editor previews 2025-08-21 15:55:21 +02:00
Davy Hélard
79a29cf7d5 Fix the "Include and libs export" time log 2025-08-21 15:33:00 +02:00
Davy Hélard
98a1323bf5 Allow to skip library files copy. 2025-08-21 15:08:19 +02:00
Davy Hélard
0fe6585897 Fix custom object hot-reloading 2025-08-18 19:14:16 +02:00
Davy Hélard
33dd605c57 Remove logs 2025-08-14 19:47:22 +02:00
Davy Hélard
bab0c227f8 Remove editedObjectDataList 2025-08-14 12:50:37 +02:00
Davy Hélard
5ad2be4a8a Remove log 2025-08-14 12:49:45 +02:00
Davy Hélard
39e18678f5 Keep the camera 2025-08-14 10:56:35 +02:00
Davy Hélard
f54b13a9f5 Avoid to hot-reload when replacing a resource with another one from the project. 2025-08-13 19:52:48 +02:00
Davy Hélard
703adc090a Avoid to call onChange when the resource name stay the same 2025-08-13 14:50:28 +02:00
Davy Hélard
81f0047dab Avoid to call triggerResourcesHaveChanged if no resource are added to the project 2025-08-12 19:14:49 +02:00
Davy Hélard
ef70add27b Format 2025-08-12 19:13:46 +02:00
Davy Hélard
d45932cc04 Allow to attach an external layout to a scene 2025-08-12 17:27:05 +02:00
Davy Hélard
081a97a5fc Remove log 2025-08-12 16:49:57 +02:00
Davy Hélard
2b3cedb441 Fast reload of project data for scene list changes 2025-08-12 16:49:37 +02:00
Davy Hélard
76426117ff Always rebuild the scene for editor hot-reload 2025-08-12 12:44:58 +02:00
Davy Hélard
e8720780eb Review changes: comment and object parameter 2025-08-11 19:30:12 +02:00
Davy Hélard
e5ed642121 Avoid to launch 2 previews when opening a project 2025-08-11 19:16:09 +02:00
Davy Hélard
a6602292b7 Remove log 2025-08-11 19:04:47 +02:00
Davy Hélard
859d8e08a0 Squash the hot-reloads for editing images when the editor dialog is closed 2025-08-11 18:31:05 +02:00
Davy Hélard
8128adfd7b Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-08-11 15:58:27 +02:00
Davy Hélard
f9317dd17f Refresh the resources without a hard-reload. 2025-08-11 15:54:50 +02:00
Davy Hélard
32a169014a Fix a crash in preview for 2D games. 2025-08-11 15:30:14 +02:00
Davy Hélard
37d3fd99eb Fix cached materials not being cleared when unloading the resource 2025-08-11 14:27:53 +02:00
Davy Hélard
e0973a8231 Clean up hot reload 2025-08-08 11:51:50 +02:00
Davy Hélard
876ce0d0a5 Add a todo for preview crash pop-up 2025-08-08 11:51:50 +02:00
Davy Hélard
9f614ce7e0 Refactor unloadResource 2025-08-08 11:51:50 +02:00
Davy Hélard
63de997e60 Fix making object as global. 2025-08-06 19:42:52 +02:00
Davy Hélard
d2aa49fd1c Rename a type 2025-08-06 16:17:12 +02:00
Davy Hélard
d355c16bdf Fix wrong preview level. 2025-08-06 16:14:04 +02:00
Davy Hélard
8c7f5d1ea8 Remove log 2025-08-06 14:59:20 +02:00
Davy Hélard
08635de08e Avoid 2 previews to run at the same time for the editor. 2025-08-06 14:56:54 +02:00
Davy Hélard
4dc8676848 Fix flow 2025-08-06 14:19:33 +02:00
Davy Hélard
164a230cbc Fix key codes for redo and cut. 2025-08-06 12:01:54 +02:00
Davy Hélard
f3dc551284 Fix delete and redo. 2025-08-06 11:53:28 +02:00
Davy Hélard
ec674e85d6 Fix the side panel from switching to instance when a hot-reload happens 2025-08-06 10:26:35 +02:00
Davy Hélard
32f47900cd Fix Scene3DTools not being exported for empty projects. 2025-08-06 10:13:11 +02:00
Davy Hélard
320774c8d8 Fix forcing perspective in 3D editor. 2025-08-05 21:55:49 +02:00
Davy Hélard
1594f44a72 Make newIDE keep track of 3D editor cameras 2025-08-05 16:04:44 +02:00
Davy Hélard
726d3a8816 Update the camera of 2d layers. 2025-08-01 18:40:09 +02:00
Davy Hélard
097f13db42 Always export Three.js for the in-game editor. 2025-08-01 18:34:31 +02:00
Davy Hélard
15585c2007 Fix event tab were focused instead of scene tab 2025-08-01 16:46:56 +02:00
Davy Hélard
ca0a2c3215 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-08-01 16:18:49 +02:00
Davy Hélard
d2820bdf2a Remove log. 2025-07-31 20:16:12 +02:00
Davy Hélard
947e5eb9a3 Fix object hot-reload when objects are added or removed. 2025-07-31 19:24:34 +02:00
Davy Hélard
5901e34f6d Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-31 17:56:31 +02:00
Davy Hélard
1c942d2f9d Fix 1st object of a type not appearing in the 3D editor. 2025-07-31 13:00:04 +02:00
Davy Hélard
b419bf8f35 Save the 2D editor camera position 2025-07-30 18:37:20 +02:00
Davy Hélard
7edfa1284a Fix object deletion 2025-07-30 16:27:10 +02:00
Davy Hélard
32e4006b2c Fix duplicate and paste 2025-07-30 15:56:28 +02:00
Davy Hélard
e773a1dbfc Fix object renaming. 2025-07-30 15:19:45 +02:00
Davy Hélard
3942214ba1 Make sure the 3D editor doesn't mess with the selection when disabled 2025-07-30 15:11:53 +02:00
Davy Hélard
461fc36401 Fix editor hot-reload no longer being triggered. 2025-07-30 11:12:24 +02:00
Davy Hélard
fa9198174e Fix the default object size. 2025-07-29 19:17:08 +02:00
Davy Hélard
96460d92d3 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-28 18:22:19 +02:00
Davy Hélard
e0d376c15b Handle background color changes. 2025-07-28 18:20:23 +02:00
Davy Hélard
46e0301dd0 Fix a crash when previewing a game. 2025-07-28 15:09:45 +02:00
Davy Hélard
14aa26c651 Fix hot-reload triggering to often. 2025-07-25 20:47:40 +02:00
Davy Hélard
ab59dfaa86 Remove useless code. 2025-07-25 19:39:15 +02:00
Davy Hélard
4841f46d95 Fix a mistake in previous commit. 2025-07-25 17:54:06 +02:00
Davy Hélard
faccc6a6f2 Handle copy and paste. 2025-07-25 17:41:04 +02:00
Florian Rival
f336e76a86 Fix size of 2D/3D toggle on small screens 2025-07-24 23:56:17 +02:00
Florian Rival
811604d15a Fix pointer events to use the 3D editor 2025-07-24 19:09:27 +02:00
Davy Hélard
dd005941e9 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-24 18:16:29 +02:00
Davy Hélard
311381332f Remove logs 2025-07-18 20:29:08 +02:00
Davy Hélard
50173c4127 Avoid to log EDIT in the history when the selection change 2025-07-18 20:13:52 +02:00
Davy Hélard
e030adbb72 Handle undo and redo. 2025-07-18 18:20:41 +02:00
Davy Hélard
3762ccf639 Factorize a log. 2025-07-18 14:55:50 +02:00
Davy Hélard
d2758400e0 Ensure no hot-reload is done in 2D mode.
Fix 3D editor not switching while in 2D mode.
2025-07-18 13:23:37 +02:00
Davy Hélard
1551ec440e Hide the diagnostic report for the in-game editor. 2025-07-17 15:05:15 +02:00
Davy Hélard
bd493e39e4 Update the 3D editor with changes done on instances with the 2D editor. 2025-07-17 14:01:23 +02:00
Davy Hélard
2fb663b63f Fix selection between 2D and 3D editors. 2025-07-17 12:03:06 +02:00
Davy Hélard
3878f35107 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-17 10:37:52 +02:00
Davy Hélard
e055c0edbf Remove some todo. 2025-07-11 19:43:14 +02:00
Davy Hélard
a02afc4c68 Fix resource reload. 2025-07-11 19:41:28 +02:00
Davy Hélard
30130df6fb Factorize 2025-07-11 17:45:56 +02:00
Davy Hélard
400cd2e22c Use a hard-reload to update asset content. 2025-07-11 17:29:07 +02:00
Davy Hélard
dc25a0e87f Remove a log 2025-07-11 14:29:15 +02:00
Davy Hélard
d21abd97a7 Allow to reload resources on hot-reload. 2025-07-11 14:14:15 +02:00
Davy Hélard
bd38eaf924 Handle all free camera keys for the switch. 2025-07-10 13:07:12 +02:00
Davy Hélard
360e98b3ee Fix to allow to reset to default size. 2025-07-10 12:45:16 +02:00
Davy Hélard
e03908da83 Fix update of instances without custom size. 2025-07-09 18:47:02 +02:00
Florian Rival
64e7afea7c Fix 2d/3d toggle way too large on large screens 2025-07-09 16:46:00 +02:00
Davy Hélard
13077f7ce8 Fix hot-reload when an effect type is changed. 2025-07-09 10:37:33 +02:00
Davy Hélard
ef68f4e7f6 Refactor camera handling. 2025-07-08 17:17:06 +02:00
Florian Rival
eaeef23bc6 Fix warning 2025-07-08 16:13:22 +02:00
Florian Rival
5a3b12a257 Ensure keys are not stuck when losing focus during in-game edition 2025-07-08 15:47:19 +02:00
Davy Hélard
794b74a90c Trigger an hot-reload when an effect is added. 2025-07-08 13:15:40 +02:00
Florian Rival
ac4a500fff Hard reload the in-game edition in case the connection to the editor is lost 2025-07-08 11:15:12 +02:00
Florian Rival
13a5939550 Add proper toggle between classic and 3d, real-time editor 2025-07-07 23:47:02 +02:00
Florian Rival
e560f0f08b Fix resizing/dragging of panels and set transparent background only for the 3D editor 2025-07-07 22:48:55 +02:00
Davy Hélard
3894720a93 Fix some original size implementation. 2025-07-07 17:19:02 +02:00
Davy Hélard
2a89b95510 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-07 16:16:27 +02:00
Florian Rival
e8e4bf07df Add checkbox to switch between editors
Also ensure custom object custom size is not reset.
TODO: getOriginalWidth and similar must be added on all objects

Fix crash because of deleted initalVariables in instance data
2025-07-07 15:58:53 +02:00
Davy Hélard
4d3b98f78b Fix camera dragging and rectangle selection conflict. 2025-07-04 15:16:26 +02:00
Davy Hélard
42ee73b046 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-03 18:08:27 +02:00
Davy Hélard
0a5df21c1b Fix an merge issue. 2025-07-02 17:27:48 +02:00
Davy Hélard
b0ea0c262b Fix an exception in 2D projects. 2025-07-02 16:56:02 +02:00
Davy Hélard
ffa9c45692 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-07-02 16:19:14 +02:00
Davy Hélard
15f6f16d22 Fix missing hot-reload when extracting a layout 2025-07-02 16:18:44 +02:00
Davy Hélard
c3240b6767 Avoid to interrupt users with hot-reload too often 2025-07-01 19:33:56 +02:00
Davy Hélard
2f7fe905b8 Fix hot-reload when installing an extension and the focused tab is not a graphical editor. 2025-07-01 14:58:46 +02:00
Davy Hélard
0de1d42d73 Fix hot-reload when adding a new object. 2025-07-01 12:25:44 +02:00
Davy Hélard
c4c8931c73 Add a toggle to disable layer effects. 2025-06-30 19:38:59 +02:00
Davy Hélard
1c9eb654ce Allow to duplicate the selection with Ctrl + draggging. 2025-06-27 19:06:27 +02:00
Davy Hélard
ee5b55cfbb Fix selected instances being updated continuously. 2025-06-27 18:10:30 +02:00
Davy Hélard
7e121526ca Drag all the selection. 2025-06-27 17:26:56 +02:00
Davy Hélard
79dfec1feb Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-27 14:06:28 +02:00
Davy Hélard
d30a278b97 Handle instance lock. 2025-06-27 14:05:36 +02:00
Davy Hélard
5ae9e80987 Avoid to move the camera when saving. 2025-06-26 18:38:07 +02:00
Davy Hélard
245fb1b3ab Force orthographic camera type. 2025-06-26 18:32:10 +02:00
Davy Hélard
bedf8a48a2 Fix hard-reload 2025-06-26 17:58:44 +02:00
Davy Hélard
3c77c9e2d8 Avoid some crashes on 2D projects. 2025-06-26 16:46:03 +02:00
Davy Hélard
92ba2c3111 Fix a crash when creating a new EBO 2025-06-26 15:18:13 +02:00
Davy Hélard
3ea754bba2 Display the selection rectangle. 2025-06-25 18:15:38 +02:00
Davy Hélard
a3429fb687 Add some TODO 2025-06-25 17:03:41 +02:00
Davy Hélard
476a107c46 Fix instance disappearing when changing of tab if they were added after an asset installation 2025-06-25 12:48:40 +02:00
Davy Hélard
f98d28bee2 Round positions 2025-06-24 16:38:04 +02:00
Davy Hélard
df5ee8732a Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-24 16:31:45 +02:00
Davy Hélard
bd500f5a90 Fix changing of variant not being applied at hot-reload 2025-06-23 18:09:26 +02:00
Davy Hélard
536518f4bb Fix hot-reload of custom objects 2025-06-23 16:19:57 +02:00
Davy Hélard
7e11936fee Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-23 10:48:55 +02:00
Davy Hélard
b0da0508e1 Format 2025-06-21 13:10:35 +02:00
Davy Hélard
48cc5a27f6 Fix hot-reload when custom objects are in an external layout. 2025-06-21 13:02:08 +02:00
Davy Hélard
d470513f84 Make sure camera tools are exported when there are 3D objects 2025-06-21 13:00:49 +02:00
Davy Hélard
baaeb4317f Disable the nagging screen for in-game editor hot-reload. 2025-06-20 14:46:15 +02:00
Davy Hélard
40156c5e74 Trigger an full data hot-reload when assets are installed. 2025-06-20 14:10:28 +02:00
Davy Hélard
da0a47b487 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-20 11:10:46 +02:00
Davy Hélard
ee12b0b372 Hot-reload events when an EBO is installed or updated. 2025-06-19 19:08:32 +02:00
Davy Hélard
31f7ec3a17 Comply variants when an extension is updated. 2025-06-19 19:06:59 +02:00
Davy Hélard
3ec7617b14 Load resources for custom object tabs. 2025-06-19 14:50:58 +02:00
Davy Hélard
69e9eebd8e Trigger an full data hot-reload when a resource is changed in an object 2025-06-18 14:39:35 +02:00
Davy Hélard
268af00ce8 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-17 10:54:35 +02:00
Davy Hélard
ad748e4ed5 Allow to drag an object on top of another one. 2025-06-13 17:28:35 +02:00
Davy Hélard
f933736f1f Fix inner area not being cleared on tab change. 2025-06-13 14:07:32 +02:00
Davy Hélard
89861de544 Fix selection in custom object editor. 2025-06-13 14:00:58 +02:00
Davy Hélard
d503fff76b Display the inner area of custom objects. 2025-06-13 13:00:17 +02:00
Davy Hélard
e8dc9e3b20 Fix variant tabs. 2025-06-12 18:35:22 +02:00
Davy Hélard
e40cdfca95 Restore camera location when changing of tab. 2025-06-12 17:34:34 +02:00
Davy Hélard
e357fca9c0 Fix selection 2025-06-12 16:57:51 +02:00
Davy Hélard
6b4f022157 Handle locked layers. 2025-06-12 16:28:01 +02:00
Davy Hélard
29c276d061 Add new instance to the selected layer. 2025-06-12 13:51:35 +02:00
Davy Hélard
b28eec1071 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-06-12 11:24:45 +02:00
Davy Hélard
c3c774579d Fix free camera vertical rotation. 2025-05-23 16:11:32 +02:00
Davy Hélard
193977d6d2 Move zoom functions next to each other. 2025-05-23 15:36:08 +02:00
Davy Hélard
931bb10edb Automatically toggle in orbit mode when a zoom action is used. 2025-05-23 15:34:15 +02:00
Davy Hélard
408b9c151c Allow to toggle between free camera or orbit. 2025-05-22 20:17:09 +02:00
Davy Hélard
bd96a63087 Filter messages according to the editorId. 2025-05-21 13:39:00 +02:00
Davy Hélard
dd5eb3b96f Format and type 2025-05-21 11:07:31 +02:00
Davy Hélard
cd46428b81 Show a box at hovering. 2025-05-20 19:16:59 +02:00
Davy Hélard
29926f057e Handle rectangular selection (without displaying the rectangle yet) 2025-05-20 17:30:29 +02:00
Davy Hélard
5444896b76 Fix tabs unregistration. 2025-05-19 17:42:32 +02:00
Davy Hélard
4adb7295bd Update InstanceData to avoid old data to override new ones when changing of tab. 2025-05-19 14:44:06 +02:00
Davy Hélard
2edd127d68 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-05-16 10:47:40 +02:00
Davy Hélard
eb45ed8b9a Avoid to create objects where they can't be seen. 2025-05-14 21:03:20 +02:00
Davy Hélard
3a81c76741 Keep layer and object changes when switching of tabs. 2025-05-14 17:13:01 +02:00
Davy Hélard
14e2a6236a Keep the selection when switching of tabs. 2025-05-14 16:13:35 +02:00
Davy Hélard
1ea065dec6 Fix registration to previewDebuggerServer when a new tab is open. 2025-05-13 15:20:19 +02:00
Davy Hélard
db42bbef62 Better margin for zoom to fit. 2025-05-13 12:40:52 +02:00
Davy Hélard
bd0b610a90 Avoid the in-game editor to crash on 2D projects. 2025-05-13 12:34:26 +02:00
Davy Hélard
f46213ce6e Fix onEditorHidden when another project is open. 2025-05-13 12:17:12 +02:00
Davy Hélard
0909e1d5fe Fix zoom to objects 2025-05-13 12:17:12 +02:00
Davy Hélard
95c15b2edb Fix camera jumps when starting to drag. 2025-05-12 18:49:04 +02:00
Davy Hélard
e48115336d Fix flow 2025-05-12 18:32:45 +02:00
Davy Hélard
daa957fa7b Fix drop-down menu no showing up when several tabs are open. 2025-05-12 18:20:38 +02:00
Davy Hélard
3d5a997633 Merge remote-tracking branch 'official/master' into test6 2025-05-12 12:31:44 +02:00
Davy Hélard
ca15d0316d Handle duplication, deletion and cutting. 2025-05-12 12:25:19 +02:00
Davy Hélard
96a9c28e44 Handle copy and paste. 2025-05-12 12:25:19 +02:00
D8H
006f8b1900 Fix grid snapping of pasted instances (#7602) 2025-05-12 12:24:55 +02:00
Davy Hélard
d576b3ede8 Remove logs. 2025-05-07 17:03:44 +02:00
Davy Hélard
195664ee09 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-05-07 16:23:58 +02:00
Davy Hélard
bf85daf354 Fix the instance position sent to the instance property editor. 2025-05-07 16:22:35 +02:00
Davy Hélard
d3192734a1 Fix the default dimensions in the instance property editor. 2025-05-07 16:20:58 +02:00
Davy Hélard
16f5ecdd0b Remove logs. 2025-05-07 15:35:17 +02:00
Davy Hélard
b17fa8e9ae Use native resolution and show the drop-down menu. 2025-05-06 18:36:47 +02:00
Davy Hélard
88bcd797e9 Fix object hot-reload for external layouts and custom objects. 2025-05-06 14:45:49 +02:00
Davy Hélard
577d605b87 Plug fast hot-reload for layers. 2025-05-06 14:25:58 +02:00
Davy Hélard
a5e29a7f14 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-05-06 10:46:48 +02:00
Davy Hélard
7f27526c7c Handle select all instances 2025-05-05 10:43:20 +02:00
Davy Hélard
f3adb4e876 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-05-05 09:57:43 +02:00
Davy Hélard
c571a65a32 Handle the instance list selection. 2025-05-02 18:11:19 +02:00
Davy Hélard
55f38231a9 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-05-02 14:16:32 +02:00
Davy Hélard
6a2e37d147 Handle zoom commands. 2025-05-01 17:16:44 +02:00
Davy Hélard
f6ec9634ce Allow to unselect objects. 2025-04-30 11:29:54 +02:00
Davy Hélard
eedb030619 Format 2025-04-30 11:28:29 +02:00
Davy Hélard
c828c2cace Allow custom objects 3D edition. 2025-04-29 18:19:12 +02:00
Davy Hélard
b4188893a0 Display the content of custom object editors. 2025-04-29 14:17:07 +02:00
Davy Hélard
8ea26ec148 Enable interaction for external layouts. 2025-04-28 12:31:37 +02:00
Davy Hélard
bf73e430f8 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-04-28 11:17:26 +02:00
Davy Hélard
343f6a572d Allow to edit custom object instances in the editor 2025-04-25 19:09:20 +02:00
Davy Hélard
66134e95ce Handle object configuration changes. 2025-04-25 15:43:58 +02:00
Davy Hélard
1ffb03ccd5 Fix controls not moving with the instance edited from the panel 2025-04-25 11:55:37 +02:00
Davy Hélard
998bf76e72 Merge commit 'c5dd26c93b65ded5ec7972699e93c57dd320360b' into move-instance-in-game-2 2025-04-24 16:12:00 +02:00
Davy Hélard
47553bc020 Display rotation and size in fields. 2025-04-22 19:07:32 +02:00
Davy Hélard
c84b2410b9 Fix the rotation. 2025-04-22 15:11:38 +02:00
Davy Hélard
fbbac73e94 Fix the toggle. 2025-04-22 14:13:00 +02:00
Davy Hélard
c2f0f9b9a3 Handle rotation and scaling. 2025-04-18 19:10:36 +02:00
Davy Hélard
4d02ae092b Fix the dragging of a new object. 2025-04-18 16:15:44 +02:00
Davy Hélard
7db2849404 Fix the raycast when objects are found in several layers. 2025-04-18 13:34:42 +02:00
Davy Hélard
d5a3b55e98 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-04-15 17:18:23 +02:00
Florian Rival
79206fbfee WIP: Drag new instance 2025-02-06 15:26:29 +01:00
Florian Rival
2089e29664 WIP: display selection boxes in 3D editor 2025-01-29 17:49:06 +01:00
Florian Rival
38c5425d0c Fix selection of 3D models 2025-01-29 14:58:48 +01:00
Florian Rival
b0d45e80de Improve outline pass rendering 2025-01-26 16:28:04 +01:00
Florian Rival
2ac5beee0c Improve camera movement in 3D space 2025-01-25 14:23:30 +01:00
Florian Rival
c99c73fa96 WIP: handle multiselection movement and camera rotation 2025-01-25 13:02:01 +01:00
Florian Rival
f796228d06 Merge branch 'master' into move-instance-in-game-2 2025-01-25 11:17:11 +01:00
Florian Rival
ca3836b3c7 WIP: Handle deletion from in-game editor 2025-01-24 13:29:41 +01:00
Florian Rival
918f8ec989 WIP: handle selection in in-game editor 2025-01-22 22:50:20 +01:00
Florian Rival
f44d9682ee WIP: Allow more changes to instances 2025-01-20 16:05:50 +01:00
Florian Rival
ce6800164d WIP: replicate mouse/trackpad movement from the existing editor 2025-01-20 15:13:39 +01:00
Florian Rival
4afb2de178 WIP: add some camera movement 2025-01-19 22:17:28 +01:00
Florian Rival
9865fd3663 Simplify InGameEditor and add camera zooming (in 2D plane) with mouse wheel 2025-01-19 18:51:37 +01:00
Florian Rival
5501527974 Merge branch 'master' into move-instance-in-game-2 2025-01-19 16:14:14 +01:00
Florian Rival
3e4826bafc Fix memory leak with outlining in-game edited object 2025-01-15 09:50:09 +01:00
AlexandreSi
d914f9f5be Add comment 2025-01-07 15:51:40 +01:00
AlexandreSi
ab8c90dd41 Send layout name to runtime game when editing instance in editor 2025-01-07 15:32:52 +01:00
AlexandreSi
8882841a8d Fix position offset 2025-01-07 15:19:24 +01:00
AlexandreSi
3d357950f9 Send message to game when updating an instance position from the editor 2025-01-07 14:51:31 +01:00
AlexandreSi
ef5d2651c0 Update instance when receiving update from game and hot reload game 2025-01-03 17:15:32 +01:00
AlexandreSi
1dd4bb9b7a Add method to get instance persistent uuid 2025-01-03 16:39:06 +01:00
AlexandreSi
682e6f6b03 Send instances updated back to editor when modifying them with transform controls 2025-01-03 15:51:55 +01:00
AlexandreSi
b468c28ae8 Improve usability of the in-game instance control 2025-01-02 14:57:31 +01:00
AlexandreSi
9fc9452a08 Better handle gizmos 2025-01-02 12:29:53 +01:00
AlexandreSi
3b176b7152 Fix cleaning of listeners and helpers 2025-01-02 11:46:56 +01:00
AlexandreSi
f1d1a9b66b Remove unused import 2025-01-02 11:27:41 +01:00
AlexandreSi
c0d2f491a8 Fix code after rebase 2025-01-02 11:27:41 +01:00
AlexandreSi
aeecce2ea8 Avoid creating transform controls on each click 2025-01-02 11:05:05 +01:00
AlexandreSi
647ee149ff Fix hovered object outline 2025-01-02 11:05:05 +01:00
AlexandreSi
22313e148a Import changes to render without step 2025-01-02 11:05:05 +01:00
AlexandreSi
31f2d7ce2e Select object on click 2025-01-02 11:03:59 +01:00
AlexandreSi
4a0efec6c2 Add TransformControls to Three addons 2025-01-02 11:03:59 +01:00
AlexandreSi
5f555df5c1 Add outline pass in layer composer 2025-01-02 11:03:59 +01:00
AlexandreSi
53a611a1b9 Add outline pass to three addons 2025-01-02 11:03:59 +01:00
AlexandreSi
14511b23af WIP 2025-01-02 11:03:59 +01:00
Florian Rival
fa2371274d Add support for external layouts and reload when a change is made in the runtime 2025-01-02 00:10:45 +01:00
Florian Rival
0aea8dfa0f WIP: refactoring and prepare for handling external layout in-game edition 2024-12-31 17:07:21 +01:00
Florian Rival
81ca18098d Fix text object pre-rendering update 2024-12-31 16:04:11 +01:00
Florian Rival
b6e44a022f Add a note 2024-12-30 20:06:38 +01:00
Florian Rival
1a8eee2477 Remove game.resumed/paused in favor of status 2024-12-30 20:00:28 +01:00
Florian Rival
d0ef92da03 Fix flow and formatting 2024-12-30 19:25:43 +01:00
Florian Rival
9c98cb3b3b Relaunch an (in-game edition) preview when a change is made in GDJS Runtime 2024-12-30 18:56:09 +01:00
Florian Rival
3681542056 Robustify in game preview state management in the editor 2024-12-30 15:38:21 +01:00
Florian Rival
7c0bf135d7 WIP: Support for rendering while paused for in game edition 2024-12-30 12:32:05 +01:00
Florian Rival
9a31dd046c WIP: robustify scene change and initial scene for in game edition 2024-12-29 17:23:20 +01:00
Florian Rival
74401a1f9c WIP: Add scene change 2024-12-29 15:50:50 +01:00
Florian Rival
cedc6ea3e9 WIP 2024-12-29 15:50:50 +01:00
332 changed files with 15430 additions and 6784 deletions

View File

@@ -813,6 +813,13 @@ gd::String PlatformExtension::GetObjectFullType(const gd::String& extensionName,
return extensionName + separator + objectName;
}
gd::String PlatformExtension::GetVariantFullType(const gd::String& extensionName,
const gd::String& objectName,
const gd::String& variantName) {
const auto& separator = GetNamespaceSeparator();
return extensionName + separator + objectName + separator + variantName;
}
gd::String PlatformExtension::GetExtensionFromFullObjectType(
const gd::String& type) {
const auto separatorIndex =

View File

@@ -663,6 +663,10 @@ class GD_CORE_API PlatformExtension {
static gd::String GetObjectFullType(const gd::String& extensionName,
const gd::String& objectName);
static gd::String GetVariantFullType(const gd::String& extensionName,
const gd::String& objectName,
const gd::String& variantName);
static gd::String GetExtensionFromFullObjectType(const gd::String& type);
static gd::String GetObjectNameFromFullObjectType(const gd::String& type);

View File

@@ -0,0 +1,23 @@
#include "UsedObjectTypeFinder.h"
#include "GDCore/Events/Instruction.h"
#include "GDCore/IDE/ProjectBrowserHelper.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
namespace gd {
bool UsedObjectTypeFinder::ScanProject(gd::Project &project,
const gd::String &objectType) {
UsedObjectTypeFinder worker(project, objectType);
gd::ProjectBrowserHelper::ExposeProjectObjects(project, worker);
return worker.hasFoundObjectType;
};
void UsedObjectTypeFinder::DoVisitObject(gd::Object &object) {
if (!hasFoundObjectType && object.GetType() == objectType) {
hasFoundObjectType = true;
}
};
} // namespace gd

View File

@@ -0,0 +1,39 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include <set>
#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h"
#include "GDCore/Extensions/Metadata/SourceFileMetadata.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
#include "GDCore/IDE/Project/ArbitraryObjectsWorker.h"
#include "GDCore/String.h"
namespace gd {
class Project;
class Object;
} // namespace gd
namespace gd {
class GD_CORE_API UsedObjectTypeFinder : public ArbitraryObjectsWorker {
public:
static bool ScanProject(gd::Project &project, const gd::String &objectType);
private:
UsedObjectTypeFinder(gd::Project &project_, const gd::String &objectType_)
: project(project_), objectType(objectType_){};
gd::Project &project;
const gd::String &objectType;
bool hasFoundObjectType = false;
// Object Visitor
void DoVisitObject(gd::Object &object) override;
};
}; // namespace gd

View File

@@ -7,7 +7,6 @@
#include <map>
#include "GDCore/CommonTools.h"
#include "GDCore/IDE/AbstractFileSystem.h"
#include "GDCore/IDE/Project/ResourcesAbsolutePathChecker.h"
#include "GDCore/IDE/Project/ResourcesMergingHelper.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Tools/Localization.h"
@@ -26,42 +25,37 @@ bool ProjectResourcesCopier::CopyAllResourcesTo(
bool preserveAbsoluteFilenames,
bool preserveDirectoryStructure) {
if (updateOriginalProject) {
gd::ProjectResourcesCopier::CopyAllResourcesTo(
originalProject, originalProject, fs, destinationDirectory,
preserveAbsoluteFilenames, preserveDirectoryStructure);
gd::ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
originalProject, fs, destinationDirectory, preserveAbsoluteFilenames,
preserveDirectoryStructure);
} else {
gd::Project clonedProject = originalProject;
gd::ProjectResourcesCopier::CopyAllResourcesTo(
originalProject, clonedProject, fs, destinationDirectory,
preserveAbsoluteFilenames, preserveDirectoryStructure);
gd::ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
clonedProject, fs, destinationDirectory, preserveAbsoluteFilenames,
preserveDirectoryStructure);
}
return true;
}
bool ProjectResourcesCopier::CopyAllResourcesTo(
gd::Project& originalProject,
gd::Project& clonedProject,
bool ProjectResourcesCopier::AdaptFilePathsAndCopyAllResourcesTo(
gd::Project& project,
AbstractFileSystem& fs,
gd::String destinationDirectory,
bool preserveAbsoluteFilenames,
bool preserveDirectoryStructure) {
// Check if there are some resources with absolute filenames
gd::ResourcesAbsolutePathChecker absolutePathChecker(originalProject.GetResourcesManager(), fs);
gd::ResourceExposer::ExposeWholeProjectResources(originalProject, absolutePathChecker);
auto projectDirectory = fs.DirNameFrom(originalProject.GetProjectFile());
auto projectDirectory = fs.DirNameFrom(project.GetProjectFile());
std::cout << "Copying all resources from " << projectDirectory << " to "
<< destinationDirectory << "..." << std::endl;
// Get the resources to be copied
gd::ResourcesMergingHelper resourcesMergingHelper(
clonedProject.GetResourcesManager(), fs);
project.GetResourcesManager(), fs);
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
resourcesMergingHelper.PreserveDirectoriesStructure(
preserveDirectoryStructure);
resourcesMergingHelper.PreserveAbsoluteFilenames(preserveAbsoluteFilenames);
gd::ResourceExposer::ExposeWholeProjectResources(clonedProject,
gd::ResourceExposer::ExposeWholeProjectResources(project,
resourcesMergingHelper);
// Copy resources

View File

@@ -50,12 +50,10 @@ class GD_CORE_API ProjectResourcesCopier {
bool preserveDirectoryStructure = true);
private:
static bool CopyAllResourcesTo(gd::Project& originalProject,
gd::Project& clonedProject,
gd::AbstractFileSystem& fs,
gd::String destinationDirectory,
bool preserveAbsoluteFilenames = true,
bool preserveDirectoryStructure = true);
static bool AdaptFilePathsAndCopyAllResourcesTo(
gd::Project &project, gd::AbstractFileSystem &fs,
gd::String destinationDirectory, bool preserveAbsoluteFilenames = true,
bool preserveDirectoryStructure = true);
};
} // namespace gd

View File

@@ -1,17 +0,0 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "ResourcesAbsolutePathChecker.h"
#include "GDCore/IDE/AbstractFileSystem.h"
#include "GDCore/String.h"
namespace gd {
void ResourcesAbsolutePathChecker::ExposeFile(gd::String& resourceFilename) {
if (fs.IsAbsolute(resourceFilename)) hasAbsoluteFilenames = true;
}
} // namespace gd

View File

@@ -1,48 +0,0 @@
/*
* GDevelop Core
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#pragma once
#include "GDCore/IDE/AbstractFileSystem.h"
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
#include "GDCore/String.h"
namespace gd {
/**
* \brief Helper used to check if a project has at least a resource with an
* absolute filename.
*
* \see ArbitraryResourceWorker
*
* \ingroup IDE
*/
class GD_CORE_API ResourcesAbsolutePathChecker
: public ArbitraryResourceWorker {
public:
ResourcesAbsolutePathChecker(gd::ResourcesManager &resourcesManager,
AbstractFileSystem &fileSystem)
: ArbitraryResourceWorker(resourcesManager), hasAbsoluteFilenames(false),
fs(fileSystem){};
virtual ~ResourcesAbsolutePathChecker(){};
/**
* Return true if there is at least a resource with an absolute filename.
*/
bool HasResourceWithAbsoluteFilenames() const {
return hasAbsoluteFilenames;
};
/**
* Check if there is a resource with an absolute path
*/
virtual void ExposeFile(gd::String& resource);
private:
bool hasAbsoluteFilenames;
AbstractFileSystem& fs;
};
} // namespace gd

View File

@@ -22,6 +22,14 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) {
resourceFullFilename = gd::AbstractFileSystem::NormalizeSeparator(
resourceFullFilename); // Protect against \ on Linux.
if (shouldUseOriginalAbsoluteFilenames) {
// There is no need to fill `newFilenames` and `oldFilenames` since the file
// location stays the same.
fs.MakeAbsolute(resourceFullFilename, baseDirectory);
resourceFilename = resourceFullFilename;
return;
}
// In the case of absolute filenames that we don't want to preserve, or
// in the case of copying files without preserving relative folders, the new
// names will be generated from the filename alone (with collision protection).

View File

@@ -3,8 +3,7 @@
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef RESOURCESMERGINGHELPER_H
#define RESOURCESMERGINGHELPER_H
#pragma once
#include <map>
#include <memory>
@@ -58,6 +57,15 @@ public:
preserveAbsoluteFilenames = preserveAbsoluteFilenames_;
};
/**
* \brief Set if the absolute filenames of original files must be used for
* any resource.
*/
void SetShouldUseOriginalAbsoluteFilenames(
bool shouldUseOriginalAbsoluteFilenames_ = true) {
shouldUseOriginalAbsoluteFilenames = shouldUseOriginalAbsoluteFilenames_;
};
/**
* \brief Return a map containing the resources old absolute filename as key,
* and the resources new filenames as value. The new filenames are relative to
@@ -93,10 +101,13 @@ public:
///< absolute (C:\MyFile.png will not be
///< transformed into a relative filename
///< (MyFile.png).
/**
* Set to true if the absolute filenames of original files must be used for
* any resource.
*/
bool shouldUseOriginalAbsoluteFilenames = false;
gd::AbstractFileSystem&
fs; ///< The gd::AbstractFileSystem used to manipulate files.
};
} // namespace gd
#endif // RESOURCESMERGINGHELPER_H

View File

@@ -6,6 +6,7 @@
#include "SceneResourcesFinder.h"
#include "GDCore/IDE/ResourceExposer.h"
#include "GDCore/Project/EventsBasedObjectVariant.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Serialization/SerializerElement.h"
@@ -27,6 +28,14 @@ std::set<gd::String> SceneResourcesFinder::FindSceneResources(gd::Project &proje
return resourceWorker.resourceNames;
}
std::set<gd::String> SceneResourcesFinder::FindEventsBasedObjectVariantResources(gd::Project &project,
gd::EventsBasedObjectVariant &variant) {
gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager());
gd::ResourceExposer::ExposeEventsBasedObjectVariantResources(project, variant, resourceWorker);
return resourceWorker.resourceNames;
}
void SceneResourcesFinder::AddUsedResource(gd::String &resourceName) {
if (resourceName.empty()) {
return;

View File

@@ -15,6 +15,7 @@ namespace gd {
class Project;
class Layout;
class SerializerElement;
class EventsBasedObjectVariant;
} // namespace gd
namespace gd {
@@ -27,7 +28,7 @@ namespace gd {
class SceneResourcesFinder : private gd::ArbitraryResourceWorker {
public:
/**
* @brief Find resource usages in a given scenes.
* @brief Find resource usages in a given scene.
*
* It doesn't include resources used globally.
*/
@@ -41,6 +42,13 @@ public:
*/
static std::set<gd::String> FindProjectResources(gd::Project &project);
/**
* @brief Find resource usages in a given events-based object variant.
*/
static std::set<gd::String>
FindEventsBasedObjectVariantResources(gd::Project &project,
gd::EventsBasedObjectVariant &variant);
virtual ~SceneResourcesFinder(){};
private:

View File

@@ -332,6 +332,12 @@ void ProjectBrowserHelper::ExposeLayoutObjects(gd::Layout &layout,
worker.Launch(layout.GetObjects());
}
void ProjectBrowserHelper::ExposeEventsBasedObjectVariantObjects(
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
gd::ArbitraryObjectsWorker &worker) {
worker.Launch(eventsBasedObjectVariant.GetObjects());
}
void ProjectBrowserHelper::ExposeProjectFunctions(
gd::Project &project, gd::ArbitraryEventsFunctionsWorker &worker) {

View File

@@ -13,6 +13,7 @@ class EventsFunctionsExtension;
class EventsFunction;
class EventsBasedBehavior;
class EventsBasedObject;
class EventsBasedObjectVariant;
class ArbitraryEventsWorker;
class ArbitraryEventsWorkerWithContext;
class ArbitraryEventsFunctionsWorker;
@@ -207,6 +208,17 @@ public:
static void ExposeLayoutObjects(gd::Layout &layout,
gd::ArbitraryObjectsWorker &worker);
/**
* \brief Call the specified worker on all ObjectContainers of the
* events-based object variant.
*
* This should be the preferred way to traverse all the objects of an
* events-based object variant.
*/
static void ExposeEventsBasedObjectVariantObjects(
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
gd::ArbitraryObjectsWorker &worker);
/**
* \brief Call the specified worker on all FunctionsContainers of the project
* (global, layouts...)

View File

@@ -116,6 +116,34 @@ void ResourceExposer::ExposeLayoutResources(
project, layout, eventWorker);
}
void ResourceExposer::ExposeEventsBasedObjectVariantResources(
gd::Project &project,
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
gd::ArbitraryResourceWorker &worker) {
// Expose object configuration resources
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
gd::ProjectBrowserHelper::ExposeEventsBasedObjectVariantObjects(
eventsBasedObjectVariant, objectWorker);
// Expose layer effect resources
auto &layers = eventsBasedObjectVariant.GetLayers();
for (std::size_t layerIndex = 0; layerIndex < layers.GetLayersCount();
layerIndex++) {
auto &layer = layers.GetLayer(layerIndex);
auto &effects = layer.GetEffects();
for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount();
effectIndex++) {
auto &effect = effects.GetEffect(effectIndex);
gd::ResourceExposer::ExposeEffectResources(project.GetCurrentPlatform(),
effect, worker);
}
}
// We don't check the events because it would cost too much to do it for every
// variant. Resource usage in events-based object events and their
// dependencies should be rare.
}
void ResourceExposer::ExposeEffectResources(
gd::Platform &platform,
gd::Effect &effect,

View File

@@ -9,10 +9,11 @@ namespace gd {
class Platform;
class Project;
class ArbitraryResourceWorker;
class EventsBasedObjectVariant;
class EventsFunctionsExtension;
class Effect;
class Layout;
} // namespace gd
} // namespace gd
namespace gd {
@@ -20,7 +21,7 @@ namespace gd {
* \brief
*/
class GD_CORE_API ResourceExposer {
public:
public:
/**
* \brief Called ( e.g. during compilation ) so as to inventory internal
* resources, sometimes update their filename or any other work or resources.
@@ -50,6 +51,14 @@ class GD_CORE_API ResourceExposer {
gd::Layout &layout,
gd::ArbitraryResourceWorker &worker);
/**
* @brief Expose the resources used in a given events-based object variant.
*/
static void ExposeEventsBasedObjectVariantResources(
gd::Project &project,
gd::EventsBasedObjectVariant &eventsBasedObjectVariant,
gd::ArbitraryResourceWorker &worker);
/**
* @brief Expose the resources used in a given effect.
*/

View File

@@ -60,6 +60,18 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
} else {
SetHasCustomDepth(false);
}
if (element.HasChild("defaultWidth") ||
element.HasAttribute("defaultWidth")) {
defaultWidth = element.GetDoubleAttribute("defaultWidth");
}
if (element.HasChild("defaultHeight") ||
element.HasAttribute("defaultHeight")) {
defaultHeight = element.GetDoubleAttribute("defaultHeight");
}
if (element.HasChild("defaultDepth") ||
element.HasAttribute("defaultDepth")) {
defaultDepth = element.GetDoubleAttribute("defaultDepth");
}
SetZOrder(element.GetIntAttribute("zOrder", 0, "plan"));
SetOpacity(element.GetIntAttribute("opacity", 255));
SetLayer(element.GetStringAttribute("layer"));
@@ -74,45 +86,51 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
if (persistentUuid.empty()) ResetPersistentUuid();
numberProperties.clear();
const SerializerElement& numberPropertiesElement =
element.GetChild("numberProperties", 0, "floatInfos");
numberPropertiesElement.ConsiderAsArrayOf("property", "Info");
for (std::size_t j = 0; j < numberPropertiesElement.GetChildrenCount(); ++j) {
gd::String name =
numberPropertiesElement.GetChild(j).GetStringAttribute("name");
double value =
numberPropertiesElement.GetChild(j).GetDoubleAttribute("value");
if (element.HasChild("numberProperties", "floatInfos")) {
const SerializerElement& numberPropertiesElement =
element.GetChild("numberProperties", 0, "floatInfos");
numberPropertiesElement.ConsiderAsArrayOf("property", "Info");
for (std::size_t j = 0; j < numberPropertiesElement.GetChildrenCount(); ++j) {
gd::String name =
numberPropertiesElement.GetChild(j).GetStringAttribute("name");
double value =
numberPropertiesElement.GetChild(j).GetDoubleAttribute("value");
// Compatibility with GD <= 5.1.164
if (name == "z") {
SetZ(value);
} else if (name == "rotationX") {
SetRotationX(value);
} else if (name == "rotationY") {
SetRotationY(value);
} else if (name == "depth") {
SetHasCustomDepth(true);
SetCustomDepth(value);
}
// end of compatibility code
else {
numberProperties[name] = value;
// Compatibility with GD <= 5.1.164
if (name == "z") {
SetZ(value);
} else if (name == "rotationX") {
SetRotationX(value);
} else if (name == "rotationY") {
SetRotationY(value);
} else if (name == "depth") {
SetHasCustomDepth(true);
SetCustomDepth(value);
}
// end of compatibility code
else {
numberProperties[name] = value;
}
}
}
stringProperties.clear();
const SerializerElement& stringPropElement =
element.GetChild("stringProperties", 0, "stringInfos");
stringPropElement.ConsiderAsArrayOf("property", "Info");
for (std::size_t j = 0; j < stringPropElement.GetChildrenCount(); ++j) {
gd::String name = stringPropElement.GetChild(j).GetStringAttribute("name");
gd::String value =
stringPropElement.GetChild(j).GetStringAttribute("value");
stringProperties[name] = value;
if (element.HasChild("stringProperties", "stringInfos")) {
const SerializerElement& stringPropElement =
element.GetChild("stringProperties", 0, "stringInfos");
stringPropElement.ConsiderAsArrayOf("property", "Info");
for (std::size_t j = 0; j < stringPropElement.GetChildrenCount(); ++j) {
gd::String name = stringPropElement.GetChild(j).GetStringAttribute("name");
gd::String value =
stringPropElement.GetChild(j).GetStringAttribute("value");
stringProperties[name] = value;
}
}
GetVariables().UnserializeFrom(
element.GetChild("initialVariables", 0, "InitialVariables"));
if (element.HasChild("initialVariables", "InitialVariables")) {
GetVariables().UnserializeFrom(
element.GetChild("initialVariables", 0, "InitialVariables"));
}
}
void InitialInstance::SerializeTo(SerializerElement& element) const {
@@ -133,6 +151,8 @@ void InitialInstance::SerializeTo(SerializerElement& element) const {
element.SetAttribute("width", GetCustomWidth());
element.SetAttribute("height", GetCustomHeight());
if (HasCustomDepth()) element.SetAttribute("depth", GetCustomDepth());
// defaultWidth, defaultHeight and defaultDepth are not serialized
// because they are evaluated by InGameEditor.
if (IsLocked()) element.SetAttribute("locked", IsLocked());
if (IsSealed()) element.SetAttribute("sealed", IsSealed());
if (ShouldKeepRatio()) element.SetAttribute("keepRatio", ShouldKeepRatio());

View File

@@ -219,6 +219,13 @@ class GD_CORE_API InitialInstance {
double GetCustomDepth() const { return depth; }
void SetCustomDepth(double depth_) { depth = depth_; }
double GetDefaultWidth() const { return defaultWidth; }
double GetDefaultHeight() const { return defaultHeight; }
double GetDefaultDepth() const { return defaultDepth; }
void SetDefaultWidth(double width_) { defaultWidth = width_; }
void SetDefaultHeight(double height_) { defaultHeight = height_; }
void SetDefaultDepth(double depth_) { defaultDepth = depth_; }
/**
* \brief Return true if the instance is locked and cannot be moved in the
* IDE.
@@ -366,7 +373,11 @@ class GD_CORE_API InitialInstance {
*/
InitialInstance& ResetPersistentUuid();
const gd::String& GetPersistentUuid() const { return persistentUuid; }
/**
* \brief Reset the persistent UUID used to recognize
* the same initial instance between serialization.
*/
const gd::String& GetPersistentUuid() const { return persistentUuid; }
///@}
private:
@@ -395,6 +406,9 @@ class GD_CORE_API InitialInstance {
double width; ///< Instance custom width
double height; ///< Instance custom height
double depth; ///< Instance custom depth
double defaultWidth = 0; ///< Instance default width as reported by InGameEditor
double defaultHeight = 0; ///< Instance default height as reported by InGameEditor
double defaultDepth = 0; ///< Instance default depth as reported by InGameEditor
gd::VariablesContainer initialVariables; ///< Instance specific variables
bool locked; ///< True if the instance is locked
bool sealed; ///< True if the instance is sealed

View File

@@ -23,6 +23,7 @@ Layer::Layer()
camera3DNearPlaneDistance(3),
camera3DFarPlaneDistance(10000),
camera3DFieldOfView(45),
camera2DPlaneMaxDrawingDistance(5000),
ambientLightColorR(200),
ambientLightColorG(200),
ambientLightColorB(200) {}
@@ -56,6 +57,8 @@ void Layer::SerializeTo(SerializerElement& element) const {
element.SetAttribute("camera3DFarPlaneDistance",
GetCamera3DFarPlaneDistance());
element.SetAttribute("camera3DFieldOfView", GetCamera3DFieldOfView());
element.SetAttribute("camera2DPlaneMaxDrawingDistance",
GetCamera2DPlaneMaxDrawingDistance());
SerializerElement& camerasElement = element.AddChild("cameras");
camerasElement.ConsiderAsArrayOf("camera");
@@ -99,6 +102,8 @@ void Layer::UnserializeFrom(const SerializerElement& element) {
"camera3DFarPlaneDistance", 10000, "threeDFarPlaneDistance"));
SetCamera3DFieldOfView(element.GetDoubleAttribute(
"camera3DFieldOfView", 45, "threeDFieldOfView"));
SetCamera2DPlaneMaxDrawingDistance(element.GetDoubleAttribute(
"camera2DPlaneMaxDrawingDistance", 5000));
cameras.clear();
SerializerElement& camerasElement = element.GetChild("cameras");

View File

@@ -182,6 +182,8 @@ class GD_CORE_API Layer {
}
double GetCamera3DFieldOfView() const { return camera3DFieldOfView; }
void SetCamera3DFieldOfView(double angle) { camera3DFieldOfView = angle; }
double GetCamera2DPlaneMaxDrawingDistance() const { return camera2DPlaneMaxDrawingDistance; }
void SetCamera2DPlaneMaxDrawingDistance(double distance) { camera2DPlaneMaxDrawingDistance = distance; }
///@}
/** \name Cameras
@@ -292,6 +294,7 @@ class GD_CORE_API Layer {
double camera3DNearPlaneDistance; ///< 3D camera frustum near plan distance
double camera3DFarPlaneDistance; ///< 3D camera frustum far plan distance
double camera3DFieldOfView; ///< 3D camera field of view (fov) in degrees
double camera2DPlaneMaxDrawingDistance; ///< Max drawing distance of the 2D plane when in the 3D world
unsigned int ambientLightColorR; ///< Ambient light color Red component
unsigned int ambientLightColorG; ///< Ambient light color Green component
unsigned int ambientLightColorB; ///< Ambient light color Blue component

View File

@@ -730,6 +730,8 @@ void Project::UnserializeFrom(const SerializerElement& element) {
SetPackageName(propElement.GetStringAttribute("packageName"));
SetTemplateSlug(propElement.GetStringAttribute("templateSlug"));
SetOrientation(propElement.GetStringAttribute("orientation", "default"));
SetEffectsHiddenInEditor(
propElement.GetBoolAttribute("areEffectsHiddenInEditor", false));
SetFolderProject(propElement.GetBoolAttribute("folderProject"));
SetLastCompilationDirectory(propElement
.GetChild("latestCompilationDirectory",
@@ -1109,6 +1111,10 @@ void Project::SerializeTo(SerializerElement& element) const {
propElement.SetAttribute("packageName", packageName);
propElement.SetAttribute("templateSlug", templateSlug);
propElement.SetAttribute("orientation", orientation);
if (areEffectsHiddenInEditor) {
propElement.SetBoolAttribute("areEffectsHiddenInEditor",
areEffectsHiddenInEditor);
}
platformSpecificAssets.SerializeTo(
propElement.AddChild("platformSpecificAssets"));
loadingScreen.SerializeTo(propElement.AddChild("loadingScreen"));
@@ -1150,6 +1156,8 @@ void Project::SerializeTo(SerializerElement& element) const {
// end of compatibility code
extensionProperties.SerializeTo(propElement.AddChild("extensionProperties"));
playableDevicesElement.AddChild("").SetStringValue("mobile");
SerializerElement& platformsElement = propElement.AddChild("platforms");
platformsElement.ConsiderAsArrayOf("platform");
@@ -1319,6 +1327,8 @@ void Project::Init(const gd::Project& game) {
sceneResourcesPreloading = game.sceneResourcesPreloading;
sceneResourcesUnloading = game.sceneResourcesUnloading;
areEffectsHiddenInEditor = game.areEffectsHiddenInEditor;
}
} // namespace gd

View File

@@ -506,6 +506,20 @@ class GD_CORE_API Project {
*/
void SetCurrentPlatform(const gd::String& platformName);
/**
* Check if the effects are shown.
*/
bool AreEffectsHiddenInEditor() const { return areEffectsHiddenInEditor; }
/**
* Define the project as playable on a mobile.
* \param enable True When false effects are not shown and a default light is
* used for 3D layers.
*/
void SetEffectsHiddenInEditor(bool enable = true) {
areEffectsHiddenInEditor = enable;
}
///@}
/** \name Factory method
@@ -1165,6 +1179,9 @@ class GD_CORE_API Project {
mutable unsigned int gdBuildVersion =
0; ///< The GD build version used the last
///< time the project was saved.
bool areEffectsHiddenInEditor =
false; ///< When false effects are not shown and a default light is used
///< for 3D layers.
};
} // namespace gd

View File

@@ -70,7 +70,9 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
currentValue = element.GetChild("value").GetStringValue();
type = element.GetChild("type").GetStringValue();
if (type == "Number") {
gd::String unitName = element.GetChild("unit").GetStringValue();
gd::String unitName = element.HasChild("unit")
? element.GetChild("unit").GetStringValue()
: "";
measurementUnit =
gd::MeasurementUnit::HasDefaultMeasurementUnitNamed(unitName)
? measurementUnit =

View File

@@ -159,15 +159,9 @@ namespace gdjs {
if (initialInstanceData.depth !== undefined) {
this.setDepth(initialInstanceData.depth);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
if (initialInstanceData.flippedZ) {
this.flipZ(initialInstanceData.flippedZ);
}
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
this.flipZ(!!initialInstanceData.flippedZ);
}
setX(x: float): void {
@@ -334,6 +328,18 @@ namespace gdjs {
this.setAngle(gdjs.toDegrees(mesh.rotation.z));
}
override getOriginalWidth(): float {
return this._originalWidth;
}
override getOriginalHeight(): float {
return this._originalHeight;
}
getOriginalDepth(): float {
return this._originalDepth;
}
getWidth(): float {
return this._width;
}
@@ -380,31 +386,6 @@ namespace gdjs {
this.getRenderer().updateSize();
}
/**
* Return the width of the object for a scale of 1.
*
* It can't be 0.
*/
_getOriginalWidth(): float {
return this._originalWidth;
}
/**
* Return the height of the object for a scale of 1.
*
* It can't be 0.
*/
_getOriginalHeight(): float {
return this._originalHeight;
}
/**
* Return the object size on the Z axis (called "depth") when the scale equals 1.
*/
_getOriginalDepth(): float {
return this._originalDepth;
}
/**
* Set the width of the object for a scale of 1.
*/

View File

@@ -11,6 +11,8 @@ namespace gdjs {
this._object = runtimeObject;
this._threeObject3D = threeObject3D;
this._threeObject3D.rotation.order = 'ZYX';
//@ts-ignore
this._threeObject3D.gdjsRuntimeObject = runtimeObject;
instanceContainer
.getLayer('')

View File

@@ -115,6 +115,12 @@ namespace gdjs {
* Rotations around X and Y are not taken into account.
*/
getUnrotatedAABBMaxZ(): number;
/**
* Return the depth of the object before any custom size is applied.
* @return The depth of the object
*/
getOriginalDepth(): float;
}
export interface Object3DDataContent {
@@ -131,7 +137,11 @@ namespace gdjs {
export namespace Base3DHandler {
export const is3D = (
object: gdjs.RuntimeObject
): object is gdjs.RuntimeObject & gdjs.Base3DHandler => {
): object is gdjs.RuntimeObject &
gdjs.Base3DHandler &
gdjs.Resizable &
gdjs.Scalable &
gdjs.Flippable => {
//@ts-ignore We are checking if the methods are present.
return object.getZ && object.setZ;
};
@@ -243,6 +253,10 @@ namespace gdjs {
getUnrotatedAABBMaxZ(): number {
return this.object.getUnrotatedAABBMaxZ();
}
getOriginalDepth(): float {
return this.object.getOriginalDepth();
}
}
gdjs.registerBehavior('Scene3D::Base3DBehavior', gdjs.Base3DBehavior);

View File

@@ -78,15 +78,9 @@ namespace gdjs {
if (initialInstanceData.depth !== undefined) {
this.setDepth(initialInstanceData.depth);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
if (initialInstanceData.flippedZ) {
this.flipZ(initialInstanceData.flippedZ);
}
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
this.flipZ(!!initialInstanceData.flippedZ);
}
getNetworkSyncData(
@@ -325,6 +319,10 @@ namespace gdjs {
return this._maxZ - this._minZ;
}
getOriginalDepth(): float {
return this._instanceContainer._getInitialInnerAreaDepth();
}
override _updateUntransformedHitBoxes(): void {
super._updateUntransformedHitBoxes();

View File

@@ -23,6 +23,8 @@ namespace gdjs {
this._threeGroup = new THREE.Group();
this._threeGroup.rotation.order = 'ZYX';
//@ts-ignore
this._threeGroup.gdjsRuntimeObject = object;
const layer = parent.getLayer('');
if (layer) {

View File

@@ -40,6 +40,19 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -221,9 +221,11 @@ namespace gdjs {
this.setWrappingWidth(initialInstanceData.width);
this.setWrapping(true);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
override onDestroyed(): void {

View File

@@ -29,6 +29,19 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

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

View File

@@ -35,6 +35,19 @@ describe('gdjs.LinksManager', function () {
stopSoundsOnStartup: false,
title: '',
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -1841,6 +1841,9 @@ namespace gdjs {
displayLoader: boolean,
openLobbiesPageIfFailure: boolean
) => {
if (runtimeScene.getGame().isInGameEdition()) {
return;
}
if (isQuickJoiningTooFast()) {
return;
}
@@ -1860,6 +1863,9 @@ namespace gdjs {
displayLoader: boolean,
openLobbiesPageIfFailure: boolean
) => {
if (runtimeScene.getGame().isInGameEdition()) {
return;
}
if (isQuickJoiningTooFast()) {
return;
}
@@ -1893,6 +1899,9 @@ namespace gdjs {
export const openLobbiesWindow = async (
runtimeScene: gdjs.RuntimeScene
) => {
if (runtimeScene.getGame().isInGameEdition()) {
return;
}
if (
isLobbiesWindowOpen(runtimeScene) ||
gdjs.playerAuthentication.isAuthenticationWindowOpen()

View File

@@ -53,6 +53,8 @@ namespace gdjs {
_renderer: gdjs.PanelSpriteRuntimeObjectRenderer;
_objectData: PanelSpriteObjectData;
/**
* @param instanceContainer The container the object belongs to.
* @param panelSpriteObjectData The initial properties of the object
@@ -62,6 +64,7 @@ namespace gdjs {
panelSpriteObjectData: PanelSpriteObjectData
) {
super(instanceContainer, panelSpriteObjectData);
this._objectData = panelSpriteObjectData;
this._rBorder = panelSpriteObjectData.rightMargin;
this._lBorder = panelSpriteObjectData.leftMargin;
this._tBorder = panelSpriteObjectData.topMargin;
@@ -84,6 +87,7 @@ namespace gdjs {
oldObjectData: PanelSpriteObjectData,
newObjectData: PanelSpriteObjectData
): boolean {
this._objectData = newObjectData;
if (oldObjectData.width !== newObjectData.width) {
this.setWidth(newObjectData.width);
}
@@ -166,9 +170,11 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
/**
@@ -247,6 +253,14 @@ namespace gdjs {
this.setHeight(newHeight);
}
override getOriginalWidth(): float {
return this._objectData.width;
}
override getOriginalHeight(): float {
return this._objectData.height;
}
setOpacity(opacity: float): void {
if (opacity < 0) {
opacity = 0;

View File

@@ -18,12 +18,15 @@ namespace gdjs {
renderer: PIXI.Container;
emitter: PIXI.particles.Emitter;
started: boolean = false;
helperGraphics: PIXI.Graphics | null = null;
runtimeObject: gdjs.ParticleEmitterObject;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
runtimeObject: gdjs.RuntimeObject,
runtimeObject: gdjs.ParticleEmitterObject,
objectData: any
) {
this.runtimeObject = runtimeObject;
const pixiRenderer = instanceContainer
.getGame()
.getRenderer()
@@ -223,6 +226,44 @@ namespace gdjs {
if (!this.started && wasEmitting) {
this.started = true;
}
if (this.helperGraphics) {
this.helperGraphics.clear();
this.helperGraphics.position.x = this.runtimeObject.getX();
this.helperGraphics.position.y = this.runtimeObject.getY();
const emitterAngle = gdjs.toRad(this.runtimeObject.getAngle());
const sprayConeAngle = gdjs.toRad(
this.runtimeObject.getConeSprayAngle()
);
const line1Angle = emitterAngle - sprayConeAngle / 2;
const line2Angle = emitterAngle + sprayConeAngle / 2;
const length = 64;
this.helperGraphics.beginFill(0, 0);
this.helperGraphics.lineStyle(
3,
this.runtimeObject.getParticleColorEnd(),
1
);
this.helperGraphics.moveTo(0, 0);
this.helperGraphics.lineTo(
Math.cos(line1Angle) * length,
Math.sin(line1Angle) * length
);
this.helperGraphics.moveTo(0, 0);
this.helperGraphics.lineTo(
Math.cos(line2Angle) * length,
Math.sin(line2Angle) * length
);
this.helperGraphics.endFill();
this.helperGraphics.lineStyle(0, 0x000000, 1);
this.helperGraphics.beginFill(
this.runtimeObject.getParticleColorStart()
);
this.helperGraphics.drawCircle(0, 0, 8);
this.helperGraphics.endFill();
}
}
setPosition(x: number, y: number): void {
@@ -443,6 +484,17 @@ namespace gdjs {
}
private static readonly frequencyMinimumValue = 0.0001;
setHelperVisible(visible: boolean) {
if (visible && !this.helperGraphics) {
this.helperGraphics = new PIXI.Graphics();
this.renderer.addChild(this.helperGraphics);
} else if (!visible && this.helperGraphics) {
this.helperGraphics.removeFromParent();
this.helperGraphics.destroy();
this.helperGraphics = null;
}
}
}
// @ts-ignore - Register the class to let the engine use it.

View File

@@ -174,6 +174,10 @@ namespace gdjs {
this,
particleObjectData
);
if (instanceContainer.getGame().isInGameEdition()) {
// TODO Disable the particles rendering
this._renderer.setHelperVisible(true);
}
this.angleA = particleObjectData.emitterAngleA;
this.angleB = particleObjectData.emitterAngleB;
this.forceMin = particleObjectData.emitterForceMin;
@@ -802,6 +806,14 @@ namespace gdjs {
}
}
getParticleColorStart(): number {
return this.color1;
}
getParticleColorEnd(): number {
return this.color2;
}
getParticleRed1(): number {
return gdjs.hexNumberToRGBArray(this.color1)[0];
}

View File

@@ -36,6 +36,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -39,6 +39,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -41,6 +41,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -623,10 +623,46 @@ Zv();a.b2Manifold.e_faceA=$v();a.b2Manifold.e_faceB=aw();a.b2_staticBody=bw();a.
})();
gdjs.registerAsynchronouslyLoadingLibraryPromise(initializeBox2D({locateFile: function(path, prefix) {
return location.protocol === 'file:' ?
// This is needed to run on Electron.
prefix + "Extensions/Physics2Behavior/" + path :
prefix + path;
// Path should always be "Box2D_v2.3.1_min.wasm.wasm" (and if it's not, we should probably hardcode it).
if (path !== 'Box2D_v2.3.1_min.wasm.wasm') {
console.warn("'path' argument sent to locateFile in Box2D_v2.3.1_min.wasm.js is not the expected string 'Box2D_v2.3.1_min.wasm.wasm'. Loading may fail.")
}
// Prefix is typically:
// Games ("exported", standalone game):
// - Web game: "https://games.gdevelop-app.com/[...]/Extensions/Physics2Behavior/"
// - Cordova Android: "https://localhost/Extensions/Physics2Behavior/".
// - Cordova iOS: "ionic://localhost/Extensions/Physics2Behavior/".
// - Electron macOS: "/private/var/[...]/Contents/Resources/app.asar/app/" (notice the missing folder).
// - Electron Windows: "C:\Users\[...]\AppData\Local\[...]\resources\app.asar\app/" (notice the missing folder).
// Preview (in the editor):
// - Web app preview (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
// - Web app preview (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
// - Electron app preview (dev editor): "/var/[...]/preview/" (notice the missing folder).
// - Electron app preview (production editor): "/var/[...]/preview/" (notice the missing folder).
// In-game editor:
// - Web app (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
// - Web app (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
// - Electron app (dev editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
// - Electron app (production editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
// If the prefix is a full URL, it's a full URL to the folder containing this JS file.
// Sill consider the case where the folder could have been missing.
let url;
if (prefix.startsWith('http:') || prefix.startsWith('https:')) {
url = prefix.endsWith('Extensions/Physics2Behavior/') ?
prefix + path :
prefix + 'Extensions/Physics2Behavior/' + path;
} else {
// Electron or Cordova iOS will fall in this case.
// We can't use this simple solution for http/https because
// on the web-app, the runtime is not necessarily hosted
// on the same domain as where the game generated files are served (so "prefix" is needed).
url = "Extensions/Physics2Behavior/" + path;
}
console.info(`Box2D wasm file is being loaded from path "${path}" with prefix "${prefix}". Resolved URL: "${url}".`);
return url;
}}).then(box2d => {
window.Box2D = box2d;
}));

View File

@@ -647,6 +647,9 @@ namespace gdjs {
export const displayAuthenticationBanner = function (
runtimeScene: gdjs.RuntimeScene
) {
if (runtimeScene.getGame().isInGameEdition()) {
return;
}
if (_authenticationBanner) {
// Banner already displayed, ensure it's visible.
_authenticationBanner.style.opacity = '1';
@@ -1042,6 +1045,10 @@ namespace gdjs {
): gdjs.PromiseTask<{ status: 'logged' | 'errored' | 'dismissed' }> =>
new gdjs.PromiseTask(
new Promise((resolve) => {
if (runtimeScene.getGame().isInGameEdition()) {
resolve({ status: 'dismissed' });
}
// Create the authentication container for the player to wait.
const domElementContainer = runtimeScene
.getGame()

View File

@@ -20,6 +20,8 @@ namespace gdjs {
_antialiasingFilter: null | PIXI.Filter = null;
_placeholder: PIXI.Sprite | null = null;
private static readonly _positionForTransformation: PIXI.IPointData = {
x: 0,
y: 0,
@@ -405,6 +407,25 @@ namespace gdjs {
updatePreRender(): void {
this.updatePositionIfNeeded();
const game = this._object.getRuntimeScene().getGame();
if (
game.isInGameEdition() &&
this._graphics.geometry.graphicsData.length === 0
) {
if (!this._placeholder) {
console.log(game.getGameData().resources.resources);
const texture = game
.getImageManager()
.getPIXITexture('InGameEditor-ShapePainterIcon');
this._placeholder = new PIXI.Sprite(texture);
}
this._graphics.addChild(this._placeholder);
} else if (this._placeholder) {
this._placeholder.removeFromParent();
this._placeholder.destroy();
this._placeholder = null;
}
}
updatePositionX(): void {

View File

@@ -278,12 +278,8 @@ namespace gdjs {
* @param initialInstanceData The extra parameters
*/
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
}
stepBehaviorsPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {

View File

@@ -50,6 +50,19 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets
instances: [],
variables: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -21,9 +21,7 @@ module.exports = {
.setExtensionInformation(
'SaveState',
_('Save State (experimental)'),
_(
'Allows to save and load the full state of a game, usually on the device storage. A Save State, by default, contains the full state of the game (objects, variables, sounds, music, effects etc.). Using the "Save Configuration" behavior, you can customize which objects should not be saved in a Save State. You can also use the "Change the save configuration of a variable" action to change the save configuration of a variable. Finally, both objects, variables and scene/game data can be given a profile name: in this case, when saving or loading with one or more profile names specified, only the object/variables/data belonging to one of the specified profiles will be saved or loaded.'
),
_('Allows to save and load the full state of a game.'),
'Neyl Mahfouf',
'Open source (MIT License)'
)
@@ -34,135 +32,65 @@ module.exports = {
extension
.addAction(
'CreateGameSaveStateInVariable',
'SaveGameSnapshotToVariable',
_('Save game to a variable'),
_(
'Create a Save State and save it to a variable. This is for advanced usage, prefer to use "Save game to device storage" in most cases.'
),
_('Save game in variable _PARAM1_ (profile(s): _PARAM2_)'),
_('Takes a snapshot of the game and save it to a variable.'),
_('Save the game in variable _PARAM1_'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable to store the save to'), '', false)
.addParameter('string', _('Profile(s) to save'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.createGameSaveStateInVariable');
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.saveVariableGameSnapshot');
extension
.addAction(
'CreateGameSaveStateInStorage',
'SaveGameSnapshotToStorage',
_('Save game to device storage'),
_('Create a Save State and save it to device storage.'),
_('Save game to device storage named _PARAM1_ (profile(s): _PARAM2_)'),
_('Takes a snapshot of the game and save it to device storage.'),
_('Save the game to device storage under key _PARAM1_'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('string', _('Storage key to save to'), '', false)
.addParameter('string', _('Profile(s) to save'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be saved. Only objects tagged with at least one of these profiles will be saved. If no profile names are specified, all objects will be saved (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.createGameSaveStateInStorage');
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.saveStorageGameSnapshot');
extension
.addAction(
'RestoreGameSaveStateFromVariable',
'LoadGameSnapshotFromVariable',
_('Load game from variable'),
_(
'Restore the game from a Save State stored in the specified variable. This is for advanced usage, prefer to use "Load game from device storage" in most cases.'
),
_(
'Load game from variable _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
),
_('Load game from a variable save snapshot.'),
_('Load the game from variable _PARAM0_'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable to load the game from'), '', false)
.addParameter('string', _('Profile(s) to load'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded (unless they have a "Save Configuration" behavior set to "Do not save").'
)
)
.addParameter(
'yesorno',
_('Stop and restart all the scenes currently played?'),
'',
true
)
.setDefaultValue('no')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromVariable');
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.loadGameFromVariableSnapshot');
extension
.addAction(
'RestoreGameSaveStateFromStorage',
'LoadGameSnapshotFromStorage',
_('Load game from device storage'),
_('Restore the game from a Save State stored on the device.'),
_(
'Load game from device storage named _PARAM1_ (profile(s): _PARAM2_, stop and restart all the scenes currently played: _PARAM3_)'
),
_('Load game from device storage save snapshot.'),
_('Load the game from device storage under key _PARAM0_.'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'string',
_('Storage name to load the game from'),
'',
false
)
.addParameter('string', _('Profile(s) to load'), '', true)
.setDefaultValue('"default"')
.setParameterLongDescription(
_(
'Comma-separated list of profile names that must be loaded. Only objects tagged with at least one of these profiles will be loaded - others will be left alone. If no profile names are specified, all objects will be loaded.'
)
)
.addParameter(
'yesorno',
_('Stop and restart all the scenes currently played?'),
'',
true
)
.setDefaultValue('no')
.addParameter('string', _('Storage key to load the game from'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.restoreGameSaveStateFromStorage');
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.loadGameFromStorageSnapshot');
extension
.addExpressionAndCondition(
@@ -176,12 +104,8 @@ module.exports = {
'',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.getSecondsSinceLastSave')
.setGetter('gdjs.saveState.getSecondsSinceLastSave');
@@ -197,12 +121,8 @@ module.exports = {
'',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.getSecondsSinceLastLoad')
.setGetter('gdjs.saveState.getSecondsSinceLastLoad');
@@ -210,276 +130,58 @@ module.exports = {
.addCondition(
'SaveJustSucceeded',
_('Save just succeeded'),
_('The last save attempt just succeeded.'),
_('Save just succeeded'),
_('the save just succeeded'),
_('the save just succeeded'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasSaveJustSucceeded');
extension
.addCondition(
'SaveJustFailed',
_('Save just failed'),
_('The last save attempt just failed.'),
_('Save just failed'),
_('the save just failed'),
_('the save just failed'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasSaveJustFailed');
extension
.addCondition(
'LoadJustSucceeded',
_('Load just succeeded'),
_('The last load attempt just succeeded.'),
_('Load just succeeded'),
_('the load just succeeded'),
_('the load just succeeded'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasLoadJustSucceeded');
extension
.addCondition(
'LoadJustFailed',
_('Load just failed'),
_('The last load attempt just failed.'),
_('Load just failed'),
_('the load just failed'),
_('the load just failed'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasLoadJustFailed');
extension
.addAction(
'SetVariableSaveConfiguration',
_('Change the save configuration of a variable'),
_(
'Set if a scene or global variable should be saved in the default save state. Also allow to specify one or more profiles in which the variable should be saved.'
),
_(
'Change save configuration of _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'variable',
_('Variable for which configuration should be changed'),
'',
false
)
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the variable should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the variable will be saved. When a save state is created with one or more profile names specified, the variable will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setVariableSaveConfiguration');
extension
.addAction(
'SetGameDataSaveConfiguration',
_('Change the save configuration of the global game data'),
_(
'Set if the global game data (audio & global variables) should be saved in the default save state. Also allow to specify one or more profiles in which the global game data should be saved.'
),
_(
'Change save configuration of global game data: save them in the default save states: _PARAM1_ and in profiles: _PARAM2_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the global game data should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the global game data will be saved. When a save state is created with one or more profile names specified, the global game data will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setGameDataSaveConfiguration');
extension
.addAction(
'SetSceneDataSaveConfiguration',
_('Change the save configuration of a scene data'),
_(
'Set if the data of the specified scene (scene variables, timers, trigger once, wait actions, layers, etc.) should be saved in the default save state. Also allow to specify one or more profiles in which the scene data should be saved. Note: objects are always saved separately from the scene data (use the "Save Configuration" behavior to customize the configuration of objects).'
),
_(
'Change save configuration of scene _PARAM1_: save it in the default save states: _PARAM2_ and in profiles: _PARAM3_'
),
_('Advanced configuration'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter(
'sceneName',
_('Scene name for which configuration should be changed'),
'',
false
)
.addParameter('yesorno', _('Persist in default save states'), '', false)
.setDefaultValue('yes')
.addParameter(
'string',
_('Profiles in which the scene data should be saved'),
'',
true
)
.setDefaultValue('')
.setParameterLongDescription(
_(
'Comma-separated list of profile names in which the scene data will be saved. When a save state is created with one or more profile names specified, the scene data will be saved only if it matches one of these profiles.'
)
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
)
.setFunctionName('gdjs.saveState.setSceneDataSaveConfiguration');
// Save Configuration behavior
const saveConfigurationBehavior = new gd.BehaviorJsImplementation();
saveConfigurationBehavior.updateProperty = function (
behaviorContent,
propertyName,
newValue
) {
if (propertyName === 'defaultProfilePersistence') {
behaviorContent
.getChild('defaultProfilePersistence')
.setStringValue(newValue);
return true;
}
if (propertyName === 'persistedInProfiles') {
behaviorContent
.getChild('persistedInProfiles')
.setStringValue(newValue);
return true;
}
return false;
};
saveConfigurationBehavior.getProperties = function (behaviorContent) {
const behaviorProperties = new gd.MapStringPropertyDescriptor();
behaviorProperties
.getOrCreate('defaultProfilePersistence')
.setValue(
behaviorContent.getChild('defaultProfilePersistence').getStringValue()
)
.setType('Choice')
.setLabel(_('Persistence mode'))
.addChoice('Persisted', _('Include in save states (default)'))
.addChoice('DoNotSave', _('Do not save'));
behaviorProperties
.getOrCreate('persistedInProfiles')
.setValue(
behaviorContent.getChild('persistedInProfiles').getStringValue()
)
.setType('String')
.setLabel(_('Save profile names'))
.setDescription(
_(
'Comma-separated list of profile names in which the object is saved. When a save state is created with one or more profile names specified, the object will be saved only if it matches one of these profiles.'
)
)
.setAdvanced(true);
return behaviorProperties;
};
saveConfigurationBehavior.initializeContent = function (behaviorContent) {
behaviorContent
.addChild('defaultProfilePersistence')
.setStringValue('Persisted');
behaviorContent.addChild('persistedInProfiles').setStringValue('');
};
const sharedData = new gd.BehaviorsSharedData();
extension
.addBehavior(
'SaveConfiguration',
_('Save state configuration'),
'SaveConfiguration',
_('Allow the customize how the object is persisted in a save state.'),
'',
'res/actions/saveUp.svg',
'SaveConfiguration',
// @ts-ignore - TODO: Fix type being a BehaviorJsImplementation instead of an Behavior
saveConfigurationBehavior,
sharedData
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setIncludeFile('Extensions/SaveState/SaveStateTools.js')
.addIncludeFile(
'Extensions/SaveState/SaveConfigurationRuntimeBehavior.js'
);
return extension;
},
runExtensionSanityTests: function (gd, extension) {

View File

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

View File

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

View File

@@ -0,0 +1,360 @@
namespace gdjs {
const logger = new gdjs.Logger('Save state');
export type LoadRequestOptions = {
loadStorageName?: string;
loadVariable?: gdjs.Variable;
};
export namespace saveState {
export const getIndexedDbDatabaseName = () => {
const gameId = gdjs.projectData.properties.projectUuid;
return `gdevelop-game-${gameId}`;
};
export const getIndexedDbObjectStore = () => {
return `game-saves`;
};
export const getIndexedDbStorageKey = (key: string) => {
return `save-${key}`;
};
const getNetworkSyncOptions: GetNetworkSyncDataOptions = {
syncObjectIdentifiers: true,
syncAllVariables: true,
syncAllBehaviors: true,
syncSceneTimers: true,
syncOnceTriggers: true,
syncSounds: true,
syncTweens: true,
syncLayers: true,
syncAsyncTasks: true,
syncSceneVisualProps: true,
syncFullTileMaps: true,
};
const updateFromNetworkSyncDataOptions: UpdateFromNetworkSyncDataOptions = {
clearSceneStack: true,
preventInitialInstancesCreation: true,
preventSoundsStoppingOnStartup: true,
clearInputs: true,
keepControl: true,
ignoreVariableOwnership: true,
};
let lastSaveTime: number | null = null;
let lastLoadTime: number | null = null;
let saveJustSucceeded: boolean = false;
let saveJustFailed: boolean = false;
let loadJustSucceeded: boolean = false;
let loadJustFailed: boolean = false;
let loadRequestOptions: LoadRequestOptions | null = null;
export const getSecondsSinceLastSave = (): number => {
if (!lastSaveTime) return -1;
return Math.floor((Date.now() - lastSaveTime) / 1000);
};
export const getSecondsSinceLastLoad = (): number => {
if (!lastLoadTime) return -1;
return Math.floor((Date.now() - lastLoadTime) / 1000);
};
export const hasSaveJustSucceeded = () => {
return saveJustSucceeded;
};
export const hasLoadJustSucceeded = () => {
return loadJustSucceeded;
};
export const hasSaveJustFailed = () => {
return saveJustFailed;
};
export const hasLoadJustFailed = () => {
return loadJustFailed;
};
export const markSaveJustSucceeded = () => {
saveJustSucceeded = true;
lastSaveTime = Date.now();
};
export const markLoadJustSucceeded = () => {
loadJustSucceeded = true;
lastLoadTime = Date.now();
};
export const markSaveJustFailed = () => {
saveJustFailed = true;
};
export const markLoadJustFailed = () => {
loadJustFailed = true;
};
// Ensure that the condition "save/load just succeeded/failed" are valid only for one frame.
gdjs.registerRuntimeScenePostEventsCallback(() => {
saveJustSucceeded = false;
saveJustFailed = false;
loadJustSucceeded = false;
loadJustFailed = false;
});
gdjs.registerRuntimeScenePostEventsCallback(
(runtimeScene: gdjs.RuntimeScene) => {
loadGameSnapshotAtTheEndOfFrameIfAny(runtimeScene);
}
);
const getGameSaveState = (runtimeScene: RuntimeScene) => {
const gameSaveState: GameSaveState = {
gameNetworkSyncData: {},
layoutNetworkSyncDatas: [],
};
const gameData = runtimeScene
.getGame()
.getNetworkSyncData(getNetworkSyncOptions);
const scenes = runtimeScene.getGame().getSceneStack().getAllScenes();
gameSaveState.gameNetworkSyncData = gameData || {};
scenes.forEach((scene, index) => {
gameSaveState.layoutNetworkSyncDatas[index] = {
sceneData: {} as LayoutNetworkSyncData,
objectDatas: {},
};
// First collect all object sync data, as they may generate unique
// identifiers like their networkId.
const sceneRuntimeObjects = scene.getAdhocListOfAllInstances();
for (const key in sceneRuntimeObjects) {
if (sceneRuntimeObjects.hasOwnProperty(key)) {
const object = sceneRuntimeObjects[key];
const objectSyncData = object.getNetworkSyncData(
getNetworkSyncOptions
);
gameSaveState.layoutNetworkSyncDatas[index].objectDatas[object.id] =
objectSyncData;
}
}
// Collect all scene data in the end.
const sceneDatas = (scene.getNetworkSyncData(getNetworkSyncOptions) ||
[]) as LayoutNetworkSyncData;
gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneDatas;
});
return gameSaveState;
};
export const saveVariableGameSnapshot = async function (
currentScene: RuntimeScene,
variable: gdjs.Variable
) {
try {
const gameSaveState = getGameSaveState(currentScene);
variable.fromJSObject(gameSaveState);
markSaveJustSucceeded();
} catch (error) {
logger.error('Error saving to variable:', error);
markSaveJustFailed();
}
};
export const saveStorageGameSnapshot = async function (
currentScene: RuntimeScene,
storageKey: string
) {
try {
const gameSaveState = getGameSaveState(currentScene);
await gdjs.indexedDb.saveToIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(storageKey),
gameSaveState
);
markSaveJustSucceeded();
} catch (error) {
logger.error('Error saving to IndexedDB:', error);
markSaveJustFailed();
}
};
export const loadGameFromVariableSnapshot = async function (
variable: gdjs.Variable
) {
// The information is saved, so that the load can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
loadRequestOptions = {
loadVariable: variable,
};
};
export const loadGameFromStorageSnapshot = async function (
storageName: string
) {
// The information is saved, so that the load can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
loadRequestOptions = {
loadStorageName: storageName,
};
};
const loadGameSnapshotAtTheEndOfFrameIfAny = function (
runtimeScene: RuntimeScene
) {
if (!loadRequestOptions) return;
const optionsToApply = loadRequestOptions;
// Reset it so we don't load it twice.
loadRequestOptions = null;
if (optionsToApply.loadVariable) {
const sceneVariables = runtimeScene.getVariables();
const variablePathInScene =
sceneVariables.getVariablePathInContainerByLoopingThroughAllVariables(
optionsToApply.loadVariable
);
const gameVariables = runtimeScene.getGame().getVariables();
const variablePathIngame =
gameVariables.getVariablePathInContainerByLoopingThroughAllVariables(
optionsToApply.loadVariable
);
const saveState =
optionsToApply.loadVariable.toJSObject() as GameSaveState;
try {
loadGameFromSave(runtimeScene, saveState, {
variableToRehydrate: optionsToApply.loadVariable,
variablePathInScene: variablePathInScene,
variablePathInGame: variablePathIngame,
});
markLoadJustSucceeded();
} catch (error) {
logger.error('Error loading from variable:', error);
markLoadJustFailed();
}
} else if (optionsToApply.loadStorageName) {
gdjs.indexedDb
.loadFromIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(optionsToApply.loadStorageName)
)
.then((jsonData) => {
const saveState = jsonData as GameSaveState;
loadGameFromSave(runtimeScene, saveState);
markLoadJustSucceeded();
})
.catch((error) => {
logger.error('Error loading from IndexedDB:', error);
markLoadJustFailed();
});
}
};
const loadGameFromSave = (
runtimeScene: RuntimeScene,
saveState: GameSaveState,
saveOptions?: {
variableToRehydrate: gdjs.Variable;
variablePathInScene: string[] | null;
variablePathInGame: string[] | null;
}
): void => {
// Save the content of the save, as it will be erased after the load.
const variableToRehydrateNetworkSyncData = saveOptions
? saveOptions.variableToRehydrate.getNetworkSyncData(
getNetworkSyncOptions
)
: null;
// First update the game, which will update the variables,
// and set the scene stack to update when ready.
const runtimeGame = runtimeScene.getGame();
runtimeGame.updateFromNetworkSyncData(
saveState.gameNetworkSyncData,
updateFromNetworkSyncDataOptions
);
// Apply the scene stack updates, as we are at the end of a frame,
// we can safely do it.
const sceneStack = runtimeGame.getSceneStack();
sceneStack.applyUpdateFromNetworkSyncDataIfAny(
updateFromNetworkSyncDataOptions
);
// Then get all scenes, which we assume will be the expected ones
// after the load has been done, so we can update them,
// and create their objects.
const runtimeScenes = sceneStack.getAllScenes();
runtimeScenes.forEach((scene, index) => {
const layoutSyncData = saveState.layoutNetworkSyncDatas[index];
if (!layoutSyncData) return;
// Create objects first, so they are available for the scene update,
// especially so that they have a networkId defined.
const objectDatas = layoutSyncData.objectDatas;
for (const id in objectDatas) {
const objectNetworkSyncData = objectDatas[id];
const objectName = objectNetworkSyncData.n;
if (!objectName) {
logger.warn('Tried to recreate an object without a name.');
continue;
}
const object = scene.createObject(objectName);
if (object) {
object.updateFromNetworkSyncData(
objectNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
// Update the scene last.
scene.updateFromNetworkSyncData(
layoutSyncData.sceneData,
updateFromNetworkSyncDataOptions
);
});
// Finally, if the save was done in a variable,
// rehydrate the variable where the save was done,
// as it has been erased by the load.
if (saveOptions && variableToRehydrateNetworkSyncData) {
const currentScene = sceneStack.getCurrentScene();
if (!currentScene) return;
const sceneVariables = currentScene.getVariables();
const gameVariables = currentScene.getGame().getVariables();
const { variablePathInScene, variablePathInGame } = saveOptions;
if (variablePathInScene && variablePathInScene.length > 0) {
const variableName =
variablePathInScene[variablePathInScene.length - 1];
const variableInScene =
sceneVariables.getVariableFromPath(variablePathInScene);
if (variableInScene) {
const variableNetworkSyncData: VariableNetworkSyncData = {
name: variableName,
...variableToRehydrateNetworkSyncData,
};
variableInScene.updateFromNetworkSyncData(
variableNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
if (variablePathInGame && variablePathInGame.length > 0) {
const variableName =
variablePathInGame[variablePathInGame.length - 1];
const variableInGame =
gameVariables.getVariableFromPath(variablePathInGame);
if (variableInGame) {
const variableNetworkSyncData: VariableNetworkSyncData = {
name: variableName,
...variableToRehydrateNetworkSyncData,
};
variableInGame.updateFromNetworkSyncData(
variableNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
}
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -203,13 +203,17 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
const loadedSpineAtlas = this._loadedSpineAtlases.getFromName(
resourceData.name
);
if (loadedSpineAtlas) {
loadedSpineAtlas.dispose();
this._loadedSpineAtlases.delete(resourceData);
}
const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
const loadingSpineAtlas = this._loadingSpineAtlases.getFromName(
resourceData.name
);
if (loadingSpineAtlas) {
loadingSpineAtlas.then((atl) => atl.dispose());
this._loadingSpineAtlases.delete(resourceData);

View File

@@ -218,15 +218,13 @@ namespace gdjs {
this.setSize(initialInstanceData.width, initialInstanceData.height);
this.invalidateHitboxes();
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
}
getDrawableX(): number {

View File

@@ -61,6 +61,19 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
instances: [],
variables: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -141,7 +141,9 @@ namespace gdjs {
);
this._borderOpacity = objectData.content.borderOpacity;
this._borderWidth = objectData.content.borderWidth;
this._disabled = objectData.content.disabled;
this._disabled = instanceContainer.getGame().isInGameEdition()
? true
: objectData.content.disabled;
this._readOnly = objectData.content.readOnly;
this._spellCheck =
objectData.content.spellCheck !== undefined
@@ -334,9 +336,11 @@ namespace gdjs {
this.setHeight(initialInstanceData.height);
this._renderer.updatePadding();
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
onScenePaused(runtimeScene: gdjs.RuntimeScene): void {
@@ -566,6 +570,9 @@ namespace gdjs {
}
setDisabled(value: boolean) {
if (this.getInstanceContainer().getGame().isInGameEdition()) {
return;
}
this._disabled = value;
this._renderer.updateDisabled();
}

View File

@@ -340,7 +340,9 @@ namespace gdjs {
return this._renderer.getRendererObject();
}
override update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
override updatePreRender(
instanceContainer: gdjs.RuntimeInstanceContainer
): void {
this._renderer.ensureUpToDate();
}
@@ -358,9 +360,11 @@ namespace gdjs {
} else {
this.setWrapping(false);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
/**

View File

@@ -236,9 +236,11 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
@@ -449,6 +451,14 @@ namespace gdjs {
return this._renderer.getHeight();
}
override getOriginalWidth(): float {
return this.getTileMapWidth();
}
override getOriginalHeight(): float {
return this.getTileMapHeight();
}
getScaleX(): float {
return this._renderer.getScaleX();
}

View File

@@ -57,6 +57,19 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -197,9 +197,11 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
private _updateTileMap(): void {
@@ -343,6 +345,14 @@ namespace gdjs {
this.setHeight(newHeight);
}
override getOriginalWidth(): float {
return this.getTileMapWidth();
}
override getOriginalHeight(): float {
return this.getTileMapHeight();
}
/**
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
*

View File

@@ -42,6 +42,8 @@ namespace gdjs {
_renderer: gdjs.TiledSpriteRuntimeObjectRenderer;
_objectData: TiledSpriteObjectData;
/**
* @param instanceContainer The container the object belongs to.
* @param tiledSpriteObjectData The initial properties of the object
@@ -51,6 +53,7 @@ namespace gdjs {
tiledSpriteObjectData: TiledSpriteObjectData
) {
super(instanceContainer, tiledSpriteObjectData);
this._objectData = tiledSpriteObjectData;
this._renderer = new gdjs.TiledSpriteRuntimeObjectRenderer(
this,
instanceContainer,
@@ -66,6 +69,7 @@ namespace gdjs {
}
updateFromObjectData(oldObjectData, newObjectData): boolean {
this._objectData = newObjectData;
if (oldObjectData.texture !== newObjectData.texture) {
this.setTexture(newObjectData.texture, this.getRuntimeScene());
}
@@ -129,9 +133,11 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
/**
@@ -223,6 +229,14 @@ namespace gdjs {
this.setHeight(height);
}
override getOriginalWidth(): float {
return this._objectData.width;
}
override getOriginalHeight(): float {
return this._objectData.height;
}
/**
* Set the offset on the X-axis when displaying the image of the Tiled Sprite object.
* @param xOffset The new offset on the X-axis.

View File

@@ -34,6 +34,19 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -151,9 +151,11 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
onDestroyed(): void {

View File

@@ -18,6 +18,9 @@
#include "GDCore/IDE/Project/ProjectResourcesCopier.h"
#include "GDCore/IDE/Project/SceneResourcesFinder.h"
#include "GDCore/IDE/ProjectStripper.h"
#include "GDCore/Project/EventsBasedObject.h"
#include "GDCore/Project/EventsBasedObjectVariant.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/ExternalEvents.h"
#include "GDCore/Project/ExternalLayout.h"
#include "GDCore/Project/Layout.h"
@@ -47,7 +50,7 @@ Exporter::~Exporter() {}
bool Exporter::ExportProjectForPixiPreview(
const PreviewExportOptions &options) {
ExporterHelper helper(fs, gdjsRoot, codeOutputDir);
return helper.ExportProjectForPixiPreview(options);
return helper.ExportProjectForPixiPreview(options, includesFiles);
}
bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
@@ -80,7 +83,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
// Prepare the export directory
fs.MkDir(exportDir);
std::vector<gd::String> includesFiles;
includesFiles.clear();
std::vector<gd::String> resourcesFiles;
// Export the resources (before generating events as some resources
@@ -98,6 +101,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
helper.AddLibsInclude(
/*pixiRenderers=*/true,
usedExtensionsResult.Has3DObjects(),
/*isInGameEditor=*/false,
/*includeWebsocketDebuggerClient=*/false,
/*includeWindowMessageDebuggerClient=*/false,
/*includeMinimalDebuggerClient=*/false,
@@ -120,7 +124,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
helper.ExportEffectIncludes(exportedProject, includesFiles);
// Export events
if (!helper.ExportEventsCode(exportedProject,
if (!helper.ExportScenesEventsCode(exportedProject,
codeOutputDir,
includesFiles,
wholeProjectDiagnosticReport,
@@ -130,29 +134,11 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
return false;
}
auto projectUsedResources =
gd::SceneResourcesFinder::FindProjectResources(exportedProject);
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < exportedProject.GetLayoutsCount();
layoutIndex++) {
auto &layout = exportedProject.GetLayout(layoutIndex);
scenesUsedResources[layout.GetName()] =
gd::SceneResourcesFinder::FindSceneResources(exportedProject, layout);
}
// Strip the project (*after* generating events as the events may use
// stripped things like objects groups...)...
gd::ProjectStripper::StripProjectForExport(exportedProject);
//...and export it
gd::SerializerElement noRuntimeGameOptions;
helper.ExportProjectData(fs,
exportedProject,
codeOutputDir + "/data.js",
helper.ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
noRuntimeGameOptions,
projectUsedResources,
scenesUsedResources);
/*isInGameEdition=*/false);
includesFiles.push_back(codeOutputDir + "/data.js");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
@@ -215,4 +201,17 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
return true;
}
void Exporter::SerializeProjectData(const gd::Project &project,
const PreviewExportOptions &options,
gd::SerializerElement &projectDataElement) {
ExporterHelper::SerializeProjectData(fs, project, options, projectDataElement);
}
void Exporter::SerializeRuntimeGameOptions(
const PreviewExportOptions &options,
gd::SerializerElement &runtimeGameOptionsElement) {
ExporterHelper::SerializeRuntimeGameOptions(
fs, gdjsRoot, options, includesFiles, runtimeGameOptionsElement);
}
} // namespace gdjs

View File

@@ -16,6 +16,7 @@ class Project;
class Layout;
class ExternalLayout;
class AbstractFileSystem;
class SerializerElement;
} // namespace gd
namespace gdjs {
struct PreviewExportOptions;
@@ -64,7 +65,33 @@ class Exporter {
codeOutputDir = codeOutputDir_;
}
private:
/**
* \brief Serialize a project without its events to JSON
*
* \param project The project to be exported
* \param options The content of the extra configuration
* \param projectDataElement The element where the project data is serialized
*/
void SerializeProjectData(const gd::Project &project,
const PreviewExportOptions &options,
gd::SerializerElement &projectDataElement);
/**
* \brief Serialize the content of the extra configuration to store
* in gdjs.runtimeGameOptions to JSON
*
* \warning `ExportProjectForPixiPreview` must be called first to serialize
* the list of scripts files.
*
* \param options The content of the extra configuration
* \param runtimeGameOptionsElement The element where the game options are
* serialized
*/
void
SerializeRuntimeGameOptions(const PreviewExportOptions &options,
gd::SerializerElement &runtimeGameOptionsElement);
private:
gd::AbstractFileSystem&
fs; ///< The abstract file system to be used for exportation.
gd::String lastError; ///< The last error that occurred.
@@ -72,6 +99,8 @@ class Exporter {
gdjsRoot; ///< The root directory of GDJS, used to copy runtime files.
gd::String codeOutputDir; ///< The directory where JS code is outputted. Will
///< be then copied to the final output directory.
std::vector<gd::String>
includesFiles; ///< The list of scripts files - useful for hot-reloading
};
} // namespace gdjs

View File

@@ -28,10 +28,13 @@
#include "GDCore/IDE/Events/UsedExtensionsFinder.h"
#include "GDCore/IDE/ExportedDependencyResolver.h"
#include "GDCore/IDE/Project/ProjectResourcesCopier.h"
#include "GDCore/IDE/Project/ResourcesMergingHelper.h"
#include "GDCore/IDE/Project/SceneResourcesFinder.h"
#include "GDCore/IDE/ProjectStripper.h"
#include "GDCore/IDE/ResourceExposer.h"
#include "GDCore/IDE/SceneNameMangler.h"
#include "GDCore/Project/EventsBasedObject.h"
#include "GDCore/Project/EventsBasedObjectVariant.h"
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/ExternalEvents.h"
#include "GDCore/Project/ExternalLayout.h"
@@ -58,7 +61,6 @@ double GetTimeSpent(double previousTime) { return GetTimeNow() - previousTime; }
double LogTimeSpent(const gd::String &name, double previousTime) {
gd::LogStatus(name + " took " + gd::String::From(GetTimeSpent(previousTime)) +
"ms");
std::cout << std::endl;
return GetTimeNow();
}
} // namespace
@@ -104,128 +106,298 @@ ExporterHelper::ExporterHelper(gd::AbstractFileSystem &fileSystem,
: fs(fileSystem), gdjsRoot(gdjsRoot_), codeOutputDir(codeOutputDir_) {};
bool ExporterHelper::ExportProjectForPixiPreview(
const PreviewExportOptions &options) {
const PreviewExportOptions &options,
std::vector<gd::String> &includesFiles) {
if (options.isInGameEdition && !options.shouldReloadProjectData &&
!options.shouldReloadLibraries && !options.shouldGenerateScenesEventsCode) {
gd::LogStatus("Skip project export entirely");
return "";
}
double previousTime = GetTimeNow();
fs.MkDir(options.exportPath);
fs.ClearDir(options.exportPath);
std::vector<gd::String> includesFiles;
if (options.shouldClearExportFolder) {
fs.ClearDir(options.exportPath);
}
includesFiles.clear();
std::vector<gd::String> resourcesFiles;
// TODO Try to remove side effects to avoid the copy
// that destroys the AST in cache.
gd::Project exportedProject = options.project;
const gd::Project &immutableProject = exportedProject;
const gd::Project &immutableProject = options.project;
previousTime = LogTimeSpent("Project cloning", previousTime);
if (options.fullLoadingScreen) {
// Use project properties fallback to set empty properties
if (exportedProject.GetAuthorIds().empty() &&
!options.fallbackAuthorId.empty()) {
exportedProject.GetAuthorIds().push_back(options.fallbackAuthorId);
}
if (exportedProject.GetAuthorUsernames().empty() &&
!options.fallbackAuthorUsername.empty()) {
exportedProject.GetAuthorUsernames().push_back(
options.fallbackAuthorUsername);
if (options.isInGameEdition) {
if (options.shouldReloadProjectData || options.shouldGenerateScenesEventsCode) {
auto projectDirectory = fs.DirNameFrom(exportedProject.GetProjectFile());
gd::ResourcesMergingHelper resourcesMergingHelper(
exportedProject.GetResourcesManager(), fs);
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
resourcesMergingHelper.SetShouldUseOriginalAbsoluteFilenames();
gd::ResourceExposer::ExposeWholeProjectResources(exportedProject,
resourcesMergingHelper);
previousTime = LogTimeSpent("Resource path resolving", previousTime);
}
gd::LogStatus("Resource export is skipped");
} else {
// Most of the time, we skip the logo and minimum duration so that
// the preview start as soon as possible.
exportedProject.GetLoadingScreen()
.ShowGDevelopLogoDuringLoadingScreen(false)
.SetMinDuration(0);
exportedProject.GetWatermark().ShowGDevelopWatermark(false);
// Export resources (*before* generating events as some resources filenames
// may be updated)
ExportResources(fs, exportedProject, options.exportPath);
previousTime = LogTimeSpent("Resource export", previousTime);
}
// Export resources (*before* generating events as some resources filenames
// may be updated)
ExportResources(fs, exportedProject, options.exportPath);
previousTime = LogTimeSpent("Resource export", previousTime);
// Compatibility with GD <= 5.0-beta56
// Stay compatible with text objects declaring their font as just a filename
// without a font resource - by manually adding these resources.
AddDeprecatedFontFilesToFontResources(
fs, exportedProject.GetResourcesManager(), options.exportPath);
// end of compatibility code
auto usedExtensionsResult =
gd::UsedExtensionsFinder::ScanProject(exportedProject);
// Export engine libraries
AddLibsInclude(/*pixiRenderers=*/true,
usedExtensionsResult.Has3DObjects(),
/*includeWebsocketDebuggerClient=*/
!options.websocketDebuggerServerAddress.empty(),
/*includeWindowMessageDebuggerClient=*/
options.useWindowMessageDebuggerClient,
/*includeMinimalDebuggerClient=*/
options.useMinimalDebuggerClient,
/*includeCaptureManager=*/
!options.captureOptions.IsEmpty(),
/*includeInAppTutorialMessage*/
!options.inAppTutorialMessageInPreview.empty(),
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
// Export files for free function, object and behaviors
for (const auto &includeFile : usedExtensionsResult.GetUsedIncludeFiles()) {
InsertUnique(includesFiles, includeFile);
}
for (const auto &requiredFile : usedExtensionsResult.GetUsedRequiredFiles()) {
InsertUnique(resourcesFiles, requiredFile);
if (options.shouldReloadProjectData || options.shouldGenerateScenesEventsCode) {
// Compatibility with GD <= 5.0-beta56
// Stay compatible with text objects declaring their font as just a filename
// without a font resource - by manually adding these resources.
AddDeprecatedFontFilesToFontResources(
fs, exportedProject.GetResourcesManager(), options.exportPath);
// end of compatibility code
}
// Export effects (after engine libraries as they auto-register themselves to
// the engine)
ExportEffectIncludes(exportedProject, includesFiles);
std::vector<gd::SourceFileMetadata> noUsedSourceFiles;
std::vector<gd::SourceFileMetadata> &usedSourceFiles = noUsedSourceFiles;
if (options.shouldReloadLibraries) {
auto usedExtensionsResult =
gd::UsedExtensionsFinder::ScanProject(exportedProject);
usedSourceFiles = usedExtensionsResult.GetUsedSourceFiles();
previousTime = LogTimeSpent("Include files export", previousTime);
// Export engine libraries
AddLibsInclude(/*pixiRenderers=*/true,
/*pixiInThreeRenderers=*/
usedExtensionsResult.Has3DObjects(),
/*isInGameEdition=*/
options.isInGameEdition,
/*includeWebsocketDebuggerClient=*/
!options.websocketDebuggerServerAddress.empty(),
/*includeWindowMessageDebuggerClient=*/
options.useWindowMessageDebuggerClient,
/*includeMinimalDebuggerClient=*/
options.useMinimalDebuggerClient,
/*includeCaptureManager=*/
!options.captureOptions.IsEmpty(),
/*includeInAppTutorialMessage*/
!options.inAppTutorialMessageInPreview.empty(),
immutableProject.GetLoadingScreen().GetGDevelopLogoStyle(),
includesFiles);
if (!options.projectDataOnlyExport) {
// Export files for free function, object and behaviors
for (const auto &includeFile : usedExtensionsResult.GetUsedIncludeFiles()) {
InsertUnique(includesFiles, includeFile);
}
for (const auto &requiredFile : usedExtensionsResult.GetUsedRequiredFiles()) {
InsertUnique(resourcesFiles, requiredFile);
}
if (options.isInGameEdition) {
// TODO Scan the objects and events of event-based objects
// (it could be an alternative method ScanProjectAndEventsBasedObjects in
// UsedExtensionsFinder).
// This is already done by UsedExtensionsFinder, but maybe it shouldn't.
// Export all event-based objects because they can be edited even if they
// are not used yet.
for (std::size_t e = 0;
e < exportedProject.GetEventsFunctionsExtensionsCount(); e++) {
auto &eventsFunctionsExtension =
exportedProject.GetEventsFunctionsExtension(e);
for (auto &&eventsBasedObjectUniquePtr :
eventsFunctionsExtension.GetEventsBasedObjects()
.GetInternalVector()) {
auto eventsBasedObject = eventsBasedObjectUniquePtr.get();
auto metadata = gd::MetadataProvider::GetExtensionAndObjectMetadata(
exportedProject.GetCurrentPlatform(),
gd::PlatformExtension::GetObjectFullType(
eventsFunctionsExtension.GetName(),
eventsBasedObject->GetName()));
for (auto &&includeFile : metadata.GetMetadata().includeFiles) {
InsertUnique(includesFiles, includeFile);
}
for (auto &behaviorType :
metadata.GetMetadata().GetDefaultBehaviors()) {
auto behaviorMetadata =
gd::MetadataProvider::GetExtensionAndBehaviorMetadata(
exportedProject.GetCurrentPlatform(), behaviorType);
for (auto &&includeFile :
behaviorMetadata.GetMetadata().includeFiles) {
InsertUnique(includesFiles, includeFile);
}
}
}
}
}
// Export effects (after engine libraries as they auto-register themselves to
// the engine)
ExportEffectIncludes(exportedProject, includesFiles);
previousTime = LogTimeSpent("Include files export", previousTime);
}
else {
gd::LogStatus("Include files export is skipped");
}
if (options.shouldGenerateScenesEventsCode) {
gd::WholeProjectDiagnosticReport &wholeProjectDiagnosticReport =
options.project.GetWholeProjectDiagnosticReport();
wholeProjectDiagnosticReport.Clear();
// Generate events code
if (!ExportEventsCode(immutableProject,
if (!ExportScenesEventsCode(immutableProject,
codeOutputDir,
includesFiles,
wholeProjectDiagnosticReport,
true)) {
return false;
}
previousTime = LogTimeSpent("Events code export", previousTime);
}
auto projectUsedResources =
gd::SceneResourcesFinder::FindProjectResources(exportedProject);
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < exportedProject.GetLayoutsCount();
layoutIndex++) {
auto &layout = exportedProject.GetLayout(layoutIndex);
scenesUsedResources[layout.GetName()] =
gd::SceneResourcesFinder::FindSceneResources(exportedProject, layout);
else {
gd::LogStatus("Events code export is skipped");
}
// Strip the project (*after* generating events as the events may use stripped
// things (objects groups...))
gd::ProjectStripper::StripProjectForExport(exportedProject);
exportedProject.SetFirstLayout(options.layoutName);
if (options.shouldReloadProjectData) {
if (options.fullLoadingScreen) {
// Use project properties fallback to set empty properties
if (exportedProject.GetAuthorIds().empty() &&
!options.fallbackAuthorId.empty()) {
exportedProject.GetAuthorIds().push_back(options.fallbackAuthorId);
}
if (exportedProject.GetAuthorUsernames().empty() &&
!options.fallbackAuthorUsername.empty()) {
exportedProject.GetAuthorUsernames().push_back(
options.fallbackAuthorUsername);
}
} else {
// Most of the time, we skip the logo and minimum duration so that
// the preview start as soon as possible.
exportedProject.GetLoadingScreen()
.ShowGDevelopLogoDuringLoadingScreen(false)
.SetMinDuration(0);
exportedProject.GetWatermark().ShowGDevelopWatermark(false);
}
previousTime = LogTimeSpent("Data stripping", previousTime);
gd::SerializerElement runtimeGameOptions;
ExporterHelper::SerializeRuntimeGameOptions(fs, gdjsRoot, options,
includesFiles, runtimeGameOptions);
ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
runtimeGameOptions, options.isInGameEdition);
includesFiles.push_back(codeOutputDir + "/data.js");
previousTime = LogTimeSpent("Project data export", previousTime);
}
else {
gd::LogStatus("Project data export is skipped");
}
if (options.shouldReloadLibraries) {
if (options.isInGameEdition) {
InsertUnique(resourcesFiles, "InGameEditor/Resources/primitivedrawingicon.png");
}
// Copy all the dependencies and their source maps
ExportIncludesAndLibs(includesFiles, options.exportPath, true);
ExportIncludesAndLibs(resourcesFiles, options.exportPath, true);
// TODO Build a full includesFiles list without actually doing export or
// generation.
if (options.shouldGenerateScenesEventsCode) {
// Create the index file
if (!ExportIndexFile(exportedProject, gdjsRoot + "/Runtime/index.html",
options.exportPath, includesFiles, usedSourceFiles,
options.nonRuntimeScriptsCacheBurst,
"gdjs.runtimeGameOptions")) {
return false;
}
}
previousTime = LogTimeSpent("Include and libs export", previousTime);
} else {
gd::LogStatus("Include and libs export is skipped");
}
return true;
}
gd::String ExporterHelper::ExportProjectData(
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition) {
fs.MkDir(fs.DirNameFrom(filename));
gd::SerializerElement projectDataElement;
ExporterHelper::StriptAndSerializeProjectData(
project, projectDataElement, isInGameEdition);
// Save the project to JSON
gd::String output =
"gdjs.projectData = " + gd::Serializer::ToJSON(projectDataElement) +
";\ngdjs.runtimeGameOptions = " + gd::Serializer::ToJSON(runtimeGameOptions) +
";\n";
if (!fs.WriteToFile(filename, output))
return "Unable to write " + filename;
return "";
}
void ExporterHelper::SerializeRuntimeGameOptions(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const PreviewExportOptions &options, std::vector<gd::String> &includesFiles,
gd::SerializerElement &runtimeGameOptions) {
// Create the setup options passed to the gdjs.RuntimeGame
gd::SerializerElement runtimeGameOptions;
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
if (!options.externalLayoutName.empty()) {
runtimeGameOptions.AddChild("injectExternalLayout")
.SetValue(options.externalLayoutName);
auto &initialRuntimeGameStatus =
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
initialRuntimeGameStatus.AddChild("sceneName")
.SetStringValue(options.layoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
initialRuntimeGameStatus.AddChild("editorId").SetValue(options.editorId);
if (!options.editorCamera3DCameraMode.empty()) {
auto &editorCamera3D =
initialRuntimeGameStatus.AddChild("editorCamera3D");
editorCamera3D.AddChild("cameraMode").SetStringValue(
options.editorCamera3DCameraMode);
editorCamera3D.AddChild("positionX")
.SetDoubleValue(options.editorCamera3DPositionX);
editorCamera3D.AddChild("positionY")
.SetDoubleValue(options.editorCamera3DPositionY);
editorCamera3D.AddChild("positionZ")
.SetDoubleValue(options.editorCamera3DPositionZ);
editorCamera3D.AddChild("rotationAngle")
.SetDoubleValue(options.editorCamera3DRotationAngle);
editorCamera3D.AddChild("elevationAngle")
.SetDoubleValue(options.editorCamera3DElevationAngle);
editorCamera3D.AddChild("distance")
.SetDoubleValue(options.editorCamera3DDistance);
}
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
if (!options.externalLayoutName.empty()) {
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
.SetValue(options.externalLayoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
.SetBoolValue(true);
}
}
if (!options.eventsBasedObjectType.empty()) {
initialRuntimeGameStatus.AddChild("eventsBasedObjectType")
.SetValue(options.eventsBasedObjectType);
initialRuntimeGameStatus.AddChild("eventsBasedObjectVariantName")
.SetValue(options.eventsBasedObjectVariantName);
}
runtimeGameOptions.AddChild("shouldReloadLibraries")
.SetBoolValue(options.shouldReloadLibraries);
runtimeGameOptions.AddChild("shouldGenerateScenesEventsCode")
.SetBoolValue(options.shouldGenerateScenesEventsCode);
runtimeGameOptions.AddChild("nativeMobileApp")
.SetBoolValue(options.nativeMobileApp);
runtimeGameOptions.AddChild("websocketDebuggerServerAddress")
@@ -297,71 +469,106 @@ bool ExporterHelper::ExportProjectForPixiPreview(
for (const auto &includeFile : includesFiles) {
auto hashIt = options.includeFileHashes.find(includeFile);
gd::String scriptSrc = GetExportedIncludeFilename(includeFile);
gd::String scriptSrc = GetExportedIncludeFilename(fs, gdjsRoot, includeFile);
scriptFilesElement.AddChild("scriptFile")
.SetStringAttribute("path", scriptSrc)
.SetIntAttribute(
"hash",
hashIt != options.includeFileHashes.end() ? hashIt->second : 0);
}
// Export the project
ExportProjectData(fs,
exportedProject,
codeOutputDir + "/data.js",
runtimeGameOptions,
projectUsedResources,
scenesUsedResources);
includesFiles.push_back(codeOutputDir + "/data.js");
previousTime = LogTimeSpent("Project data export", previousTime);
// Copy all the dependencies and their source maps
ExportIncludesAndLibs(includesFiles, options.exportPath, true);
ExportIncludesAndLibs(resourcesFiles, options.exportPath, true);
// Create the index file
if (!ExportIndexFile(exportedProject,
gdjsRoot + "/Runtime/index.html",
options.exportPath,
includesFiles,
usedExtensionsResult.GetUsedSourceFiles(),
options.nonRuntimeScriptsCacheBurst,
"gdjs.runtimeGameOptions"))
return false;
previousTime = LogTimeSpent("Include and libs export", previousTime);
return true;
}
gd::String ExporterHelper::ExportProjectData(
gd::AbstractFileSystem &fs,
gd::Project &project,
gd::String filename,
const gd::SerializerElement &runtimeGameOptions,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
fs.MkDir(fs.DirNameFrom(filename));
void ExporterHelper::AddInGameEditorResources(
gd::Project &project, std::set<gd::String> &projectUsedResources) {
project.GetResourcesManager().AddResource(
"InGameEditor-ShapePainterIcon",
"InGameEditor/Resources/primitivedrawingicon.png", "image");
projectUsedResources.insert("InGameEditor-ShapePainterIcon");
}
void ExporterHelper::SerializeProjectData(gd::AbstractFileSystem &fs,
const gd::Project &project,
const PreviewExportOptions &options,
gd::SerializerElement &rootElement) {
gd::Project clonedProject = project;
// Replace all resource file paths with the one used in exported projects.
auto projectDirectory = fs.DirNameFrom(project.GetProjectFile());
gd::ResourcesMergingHelper resourcesMergingHelper(
clonedProject.GetResourcesManager(), fs);
resourcesMergingHelper.SetBaseDirectory(projectDirectory);
if (options.isInGameEdition) {
resourcesMergingHelper.SetShouldUseOriginalAbsoluteFilenames();
} else {
resourcesMergingHelper.PreserveDirectoriesStructure(false);
resourcesMergingHelper.PreserveAbsoluteFilenames(false);
}
gd::ResourceExposer::ExposeWholeProjectResources(clonedProject,
resourcesMergingHelper);
ExporterHelper::StriptAndSerializeProjectData(
clonedProject, rootElement, options.isInGameEdition);
}
void ExporterHelper::StriptAndSerializeProjectData(
gd::Project &project, gd::SerializerElement &rootElement,
bool isInGameEdition) {
auto projectUsedResources =
gd::SceneResourcesFinder::FindProjectResources(project);
if (isInGameEdition) {
ExporterHelper::AddInGameEditorResources(project, projectUsedResources);
}
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < project.GetLayoutsCount(); layoutIndex++) {
auto &layout = project.GetLayout(layoutIndex);
scenesUsedResources[layout.GetName()] =
gd::SceneResourcesFinder::FindSceneResources(project, layout);
}
std::unordered_map<gd::String, std::set<gd::String>>
eventsBasedObjectVariantsUsedResources;
for (std::size_t extensionIndex = 0;
extensionIndex < project.GetEventsFunctionsExtensionsCount();
extensionIndex++) {
auto &eventsFunctionsExtension =
project.GetEventsFunctionsExtension(extensionIndex);
for (auto &&eventsBasedObject :
eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
auto eventsBasedObjectType = gd::PlatformExtension::GetObjectFullType(
eventsFunctionsExtension.GetName(), eventsBasedObject->GetName());
eventsBasedObjectVariantsUsedResources[eventsBasedObjectType] =
gd::SceneResourcesFinder::FindEventsBasedObjectVariantResources(
project, eventsBasedObject->GetDefaultVariant());
for (auto &&eventsBasedObjectVariant :
eventsBasedObject->GetVariants().GetInternalVector()) {
auto variantType = gd::PlatformExtension::GetVariantFullType(
eventsFunctionsExtension.GetName(), eventsBasedObject->GetName(),
eventsBasedObjectVariant->GetName());
eventsBasedObjectVariantsUsedResources[variantType] =
gd::SceneResourcesFinder::FindEventsBasedObjectVariantResources(
project, *eventsBasedObjectVariant);
}
}
}
// Strip the project (*after* generating events as the events may use stripped
// things (objects groups...))
gd::ProjectStripper::StripProjectForExport(project);
// Save the project to JSON
gd::SerializerElement rootElement;
project.SerializeTo(rootElement);
SerializeUsedResources(
rootElement, projectUsedResources, scenesUsedResources);
gd::String output =
"gdjs.projectData = " + gd::Serializer::ToJSON(rootElement) + ";\n" +
"gdjs.runtimeGameOptions = " +
gd::Serializer::ToJSON(runtimeGameOptions) + ";\n";
if (!fs.WriteToFile(filename, output)) return "Unable to write " + filename;
return "";
SerializeUsedResources(rootElement, projectUsedResources, scenesUsedResources,
eventsBasedObjectVariantsUsedResources);
}
void ExporterHelper::SerializeUsedResources(
gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources) {
auto serializeUsedResources =
[](gd::SerializerElement &element,
std::set<gd::String> &usedResources) -> void {
@@ -385,6 +592,41 @@ void ExporterHelper::SerializeUsedResources(
auto &layoutUsedResources = scenesUsedResources[layoutName];
serializeUsedResources(layoutElement, layoutUsedResources);
}
auto &extensionsElement = rootElement.GetChild("eventsFunctionsExtensions");
for (std::size_t extensionIndex = 0;
extensionIndex < extensionsElement.GetChildrenCount();
extensionIndex++) {
auto &extensionElement = extensionsElement.GetChild(extensionIndex);
const auto extensionName = extensionElement.GetStringAttribute("name");
auto &objectsElement = extensionElement.GetChild("eventsBasedObjects");
for (std::size_t objectIndex = 0;
objectIndex < objectsElement.GetChildrenCount(); objectIndex++) {
auto &objectElement = objectsElement.GetChild(objectIndex);
const auto objectName = objectElement.GetStringAttribute("name");
auto eventsBasedObjectType =
gd::PlatformExtension::GetObjectFullType(extensionName, objectName);
auto &objectUsedResources =
eventsBasedObjectVariantsUsedResources[eventsBasedObjectType];
serializeUsedResources(objectElement, objectUsedResources);
auto &variantsElement = objectElement.GetChild("variants");
for (std::size_t variantIndex = 0;
variantIndex < variantsElement.GetChildrenCount(); variantIndex++) {
auto &variantElement = variantsElement.GetChild(variantIndex);
const auto variantName = variantElement.GetStringAttribute("name");
auto variantType = gd::PlatformExtension::GetVariantFullType(
extensionName, objectName, variantName);
auto &variantUsedResources =
eventsBasedObjectVariantsUsedResources[variantType];
serializeUsedResources(variantElement, variantUsedResources);
}
}
}
}
bool ExporterHelper::ExportIndexFile(
@@ -775,7 +1017,7 @@ bool ExporterHelper::CompleteIndexFile(
gd::String codeFilesIncludes;
for (auto &include : includesFiles) {
gd::String scriptSrc =
GetExportedIncludeFilename(include, nonRuntimeScriptsCacheBurst);
GetExportedIncludeFilename(fs, gdjsRoot, include, nonRuntimeScriptsCacheBurst);
// Sanity check if the file exists - if not skip it to avoid
// including it in the list of scripts.
@@ -801,6 +1043,7 @@ bool ExporterHelper::CompleteIndexFile(
void ExporterHelper::AddLibsInclude(bool pixiRenderers,
bool pixiInThreeRenderers,
bool isInGameEdition,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
@@ -878,6 +1121,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "debugger-client/hot-reloader.js");
InsertUnique(includesFiles, "debugger-client/abstract-debugger-client.js");
InsertUnique(includesFiles, "debugger-client/InGameDebugger.js");
InsertUnique(includesFiles, "InGameEditor/InGameEditor.js");
}
if (includeWebsocketDebuggerClient) {
InsertUnique(includesFiles, "debugger-client/websocket-debugger-client.js");
@@ -890,14 +1134,16 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "debugger-client/minimal-debugger-client.js");
}
if (pixiInThreeRenderers) {
if (pixiInThreeRenderers || isInGameEdition) {
InsertUnique(includesFiles, "pixi-renderers/three.js");
InsertUnique(includesFiles, "pixi-renderers/ThreeAddons.js");
InsertUnique(includesFiles, "pixi-renderers/draco/gltf/draco_decoder.wasm");
InsertUnique(includesFiles,
"pixi-renderers/draco/gltf/draco_wasm_wrapper.js");
// Extensions in JS may use it.
InsertUnique(includesFiles, "Extensions/3D/Scene3DTools.js");
}
if (pixiRenderers) {
if (pixiRenderers || isInGameEdition) {
InsertUnique(includesFiles, "pixi-renderers/pixi.js");
InsertUnique(includesFiles, "pixi-renderers/pixi-filters-tools.js");
InsertUnique(includesFiles, "pixi-renderers/runtimegame-pixi-renderer.js");
@@ -921,7 +1167,12 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
includesFiles,
"fontfaceobserver-font-manager/fontfaceobserver-font-manager.js");
}
if (pixiInThreeRenderers) {
if (isInGameEdition) {
// `InGameEditor` uses the `is3D` function.
InsertUnique(includesFiles, "Extensions/3D/Base3DBehavior.js");
InsertUnique(includesFiles, "Extensions/3D/HemisphereLight.js");
}
if (pixiInThreeRenderers || isInGameEdition) {
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3D.js");
InsertUnique(includesFiles, "Extensions/3D/A_RuntimeObject3DRenderer.js");
InsertUnique(includesFiles, "Extensions/3D/CustomRuntimeObject3D.js");
@@ -959,7 +1210,7 @@ bool ExporterHelper::ExportEffectIncludes(
return true;
}
bool ExporterHelper::ExportEventsCode(
bool ExporterHelper::ExportScenesEventsCode(
const gd::Project &project,
gd::String outputDir,
std::vector<gd::String> &includesFiles,
@@ -995,6 +1246,7 @@ bool ExporterHelper::ExportEventsCode(
}
gd::String ExporterHelper::GetExportedIncludeFilename(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst) {
auto addSearchParameterToUrl = [](const gd::String &url,
const gd::String &urlEncodedParameterName,

View File

@@ -42,9 +42,9 @@ struct PreviewExportOptions {
useWindowMessageDebuggerClient(false),
useMinimalDebuggerClient(false),
nativeMobileApp(false),
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
isInGameEdition(false),
nonRuntimeScriptsCacheBurst(0),
inAppTutorialMessageInPreview(""),
inAppTutorialMessagePositionInPreview(""),
@@ -145,6 +145,26 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set the (optional) events-based object to be instantiated in the scene
* at the beginning of the previewed game.
*/
PreviewExportOptions &SetEventsBasedObjectType(
const gd::String &eventsBasedObjectType_) {
eventsBasedObjectType = eventsBasedObjectType_;
return *this;
}
/**
* \brief Set the (optional) events-based object variant to be instantiated in the scene
* at the beginning of the previewed game.
*/
PreviewExportOptions &SetEventsBasedObjectVariantName(
const gd::String &eventsBasedObjectVariantName_) {
eventsBasedObjectVariantName = eventsBasedObjectVariantName_;
return *this;
}
/**
* \brief Set the hash associated to an include file. Useful for the preview
* hot-reload, to know if a file changed.
@@ -156,11 +176,34 @@ struct PreviewExportOptions {
}
/**
* \brief Set if the export should only export the project data, not
* exporting events code.
* \brief Set if the exported folder should be cleared befor the export.
*/
PreviewExportOptions &SetProjectDataOnlyExport(bool enable) {
projectDataOnlyExport = enable;
PreviewExportOptions &SetShouldClearExportFolder(bool enable) {
shouldClearExportFolder = enable;
return *this;
}
/**
* \brief Set if the `ProjectData` must be reloaded.
*/
PreviewExportOptions &SetShouldReloadProjectData(bool enable) {
shouldReloadProjectData = enable;
return *this;
}
/**
* \brief Set if GDJS libraries must be reloaded.
*/
PreviewExportOptions &SetShouldReloadLibraries(bool enable) {
shouldReloadLibraries = enable;
return *this;
}
/**
* \brief Set if the export should export events code.
*/
PreviewExportOptions &SetShouldGenerateScenesEventsCode(bool enable) {
shouldGenerateScenesEventsCode = enable;
return *this;
}
@@ -182,6 +225,40 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set if the export is made for being edited in the editor.
*/
PreviewExportOptions &SetIsInGameEdition(bool enable) {
isInGameEdition = enable;
return *this;
}
/**
* \brief Set the in-game editor identifier.
*/
PreviewExportOptions &SetEditorId(const gd::String &editorId_) {
editorId = editorId_;
return *this;
}
/**
* \brief Set the camera state to use in the in-game editor.
*/
PreviewExportOptions &
SetEditorCameraState3D(const gd::String &cameraMode, double positionX,
double positionY, double positionZ,
double rotationAngle, double elevationAngle,
double distance) {
editorCamera3DCameraMode = cameraMode;
editorCamera3DPositionX = positionX;
editorCamera3DPositionY = positionY;
editorCamera3DPositionZ = positionZ;
editorCamera3DRotationAngle = rotationAngle;
editorCamera3DElevationAngle = elevationAngle;
editorCamera3DDistance = distance;
return *this;
}
/**
* \brief If set to a non zero value, the exported script URLs will have an
* extra search parameter added (with the given value) to ensure browser cache
@@ -294,6 +371,8 @@ struct PreviewExportOptions {
bool useMinimalDebuggerClient;
gd::String layoutName;
gd::String externalLayoutName;
gd::String eventsBasedObjectType;
gd::String eventsBasedObjectVariantName;
gd::String fallbackAuthorUsername;
gd::String fallbackAuthorId;
gd::String playerId;
@@ -303,9 +382,21 @@ struct PreviewExportOptions {
gd::String inAppTutorialMessagePositionInPreview;
bool nativeMobileApp;
std::map<gd::String, int> includeFileHashes;
bool projectDataOnlyExport;
bool shouldClearExportFolder = true;
bool shouldReloadProjectData = true;
bool shouldReloadLibraries = true;
bool shouldGenerateScenesEventsCode = true;
bool fullLoadingScreen;
bool isDevelopmentEnvironment;
bool isInGameEdition;
gd::String editorId;
gd::String editorCamera3DCameraMode;
double editorCamera3DPositionX = 0;
double editorCamera3DPositionY = 0;
double editorCamera3DPositionZ = 0;
double editorCamera3DRotationAngle = 0;
double editorCamera3DElevationAngle = 0;
double editorCamera3DDistance = 0;
unsigned int nonRuntimeScriptsCacheBurst;
gd::String electronRemoteRequirePath;
gd::String gdevelopResourceToken;
@@ -379,23 +470,51 @@ class ExporterHelper {
const gd::String &GetLastError() const { return lastError; };
/**
* \brief Export a project to JSON
* \brief Export a project without its events and options to 2 JS variables
*
* \param fs The abstract file system to use to write the file
* \param project The project to be exported.
* \param filename The filename where export the project
* \param runtimeGameOptions The content of the extra configuration to store
* in gdjs.runtimeGameOptions \return Empty string if everything is ok,
* in gdjs.runtimeGameOptions
*
* \return Empty string if everything is ok,
* description of the error otherwise.
*/
static gd::String ExportProjectData(
gd::AbstractFileSystem &fs,
gd::Project &project,
gd::String filename,
const gd::SerializerElement &runtimeGameOptions,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources);
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition);
/**
* \brief Serialize a project without its events to JSON
*
* \param fs The abstract file system to use to write the file
* \param project The project to be exported.
* \param options The content of the extra configuration
* \param projectDataElement The element where the project data is serialized
*/
static void SerializeProjectData(gd::AbstractFileSystem &fs,
const gd::Project &project,
const PreviewExportOptions &options,
gd::SerializerElement &projectDataElement);
/**
* \brief Serialize the content of the extra configuration to store
* in gdjs.runtimeGameOptions to JSON
*
* \param fs The abstract file system to use to write the file
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
* \param options The content of the extra configuration
* \param includesFiles The list of scripts files - useful for hot-reloading
* \param runtimeGameOptionsElement The element where the game options are
* serialized
*/
static void
SerializeRuntimeGameOptions(gd::AbstractFileSystem &fs,
const gd::String &gdjsRoot,
const PreviewExportOptions &options,
std::vector<gd::String> &includesFiles,
gd::SerializerElement &runtimeGameOptionsElement);
/**
* \brief Copy all the resources of the project to to the export directory,
@@ -416,6 +535,7 @@ class ExporterHelper {
*/
void AddLibsInclude(bool pixiRenderers,
bool pixiInThreeRenderers,
bool isInGameEdition,
bool includeWebsocketDebuggerClient,
bool includeWindowMessageDebuggerClient,
bool includeMinimalDebuggerClient,
@@ -453,7 +573,7 @@ class ExporterHelper {
* includesFiles A reference to a vector that will be filled with JS files to
* be exported along with the project. ( including "codeX.js" files ).
*/
bool ExportEventsCode(
bool ExportScenesEventsCode(
const gd::Project &project,
gd::String outputDir,
std::vector<gd::String> &includesFiles,
@@ -578,14 +698,20 @@ class ExporterHelper {
* a browser pointing to the preview.
*
* \param options The options to generate the preview.
* \param includesFiles The list of scripts files - useful for hot-reloading
*/
bool ExportProjectForPixiPreview(const PreviewExportOptions &options);
bool ExportProjectForPixiPreview(const PreviewExportOptions &options,
std::vector<gd::String> &includesFiles);
/**
* \brief Given an include file, returns the name of the file to reference
* in the exported game.
*
* \param fs The abstract file system to use
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
*/
gd::String GetExportedIncludeFilename(
static gd::String GetExportedIncludeFilename(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const gd::String &include, unsigned int nonRuntimeScriptsCacheBurst = 0);
/**
@@ -612,11 +738,36 @@ class ExporterHelper {
///< be then copied to the final output directory.
private:
static void SerializeUsedResources(
gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources);
static void
SerializeUsedResources(gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources);
/**
* \brief Stript a project and serialize it to JSON
*
* \param project The project to be exported.
*/
static void StriptAndSerializeProjectData(gd::Project &project,
gd::SerializerElement &rootElement,
bool isInGameEdition);
/**
* \brief Add additional resources that are used by the in-game editor to the
* project.
*
* \param project The project to be exported where resource declarations are
* added.
*
* \param projectUsedResources The list of resource to be loaded
* globally by the runtime.
*/
static void
AddInGameEditorResources(gd::Project &project,
std::set<gd::String> &projectUsedResources);
};
} // namespace gdjs

View File

@@ -100,18 +100,15 @@ namespace gdjs {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): AsyncTasksManagerNetworkSyncData {
const tasksData = this.tasksWithCallback
.map(({ asyncTask, callbackId, longLivedObjectsList }) => {
// Stay compatible with old extensions declaring tasks without a getNetworkSyncData method.
if (!asyncTask.getNetworkSyncData) return null;
const tasksData = this.tasksWithCallback.map(
({ asyncTask, callbackId, longLivedObjectsList }) => {
return {
callbackId,
asyncTask: asyncTask.getNetworkSyncData(),
objectsList: longLivedObjectsList.getNetworkSyncData(syncOptions),
};
})
.filter(Boolean);
}
);
return {
tasks: tasksData,

View File

@@ -13,7 +13,7 @@ namespace gdjs {
export type CustomObjectConfiguration = ObjectConfiguration & {
animatable?: SpriteAnimationData[];
variant: string;
childrenContent: { [objectName: string]: ObjectConfiguration & any };
childrenContent?: { [objectName: string]: ObjectConfiguration & any };
isInnerAreaFollowingParentSize: boolean;
};
@@ -118,37 +118,19 @@ namespace gdjs {
);
return;
}
if (!eventsBasedObjectData.defaultVariant) {
eventsBasedObjectData.defaultVariant = {
...eventsBasedObjectData,
name: '',
};
}
// Legacy events-based objects don't have any instance in their default
// variant since there wasn't a graphical editor at the time.
// In this case, the editor doesn't allow to choose a variant, but a
// variant may have stayed after a user rolled back the extension.
// This variant must be ignored to match what the editor shows.
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
eventsBasedObjectData.defaultVariant.instances.length == 0;
let usedVariantData: EventsBasedObjectVariantData =
eventsBasedObjectData.defaultVariant;
if (
customObjectData.variant &&
!isForcedToOverrideEventsBasedObjectChildrenConfiguration
) {
for (
let variantIndex = 0;
variantIndex < eventsBasedObjectData.variants.length;
variantIndex++
) {
const variantData = eventsBasedObjectData.variants[variantIndex];
if (variantData.name === customObjectData.variant) {
usedVariantData = variantData;
break;
}
}
const usedVariantData: EventsBasedObjectVariantData | null =
this.getRuntimeScene()
.getGame()
.getEventsBasedObjectVariantData(
customObjectData.type,
customObjectData.variant
);
if (!usedVariantData) {
// This can't actually happen.
logger.error(
`Unknown variant "${customObjectData.variant}" for object "${customObjectData.type}".`
);
return;
}
this._isInnerAreaFollowingParentSize =
@@ -178,8 +160,7 @@ namespace gdjs {
override reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
super.reinitialize(objectData);
this._reinitializeRenderer();
this._initializeFromObjectData(objectData);
this._reinitializeContentFromObjectData(objectData);
// When changing the variant, the instance is like a new instance.
// We call again `onCreated` at the end, like done by the constructor
@@ -187,6 +168,14 @@ namespace gdjs {
this.onCreated();
}
private _reinitializeContentFromObjectData(
objectData: ObjectData & CustomObjectConfiguration
) {
this._reinitializeRenderer();
this._instanceContainer._unloadContent();
this._initializeFromObjectData(objectData);
}
override updateFromObjectData(
oldObjectData: ObjectData & CustomObjectConfiguration,
newObjectData: ObjectData & CustomObjectConfiguration
@@ -214,8 +203,7 @@ namespace gdjs {
this._instanceContainer._initialInnerArea.max[1] !==
this._innerArea.max[1]);
this._reinitializeRenderer();
this._initializeFromObjectData(newObjectData);
this._reinitializeContentFromObjectData(newObjectData);
// The generated code calls the onCreated super implementation at the end.
this.onCreated();
@@ -311,15 +299,13 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
}
override onDeletedFromScene(): void {
@@ -658,6 +644,20 @@ namespace gdjs {
return this._unrotatedAABB.max[1];
}
getOriginalWidth(): float {
return (
this._instanceContainer.getInitialUnrotatedViewportMaxX() -
this._instanceContainer.getInitialUnrotatedViewportMinX()
);
}
getOriginalHeight(): float {
return (
this._instanceContainer.getInitialUnrotatedViewportMaxY() -
this._instanceContainer.getInitialUnrotatedViewportMinY()
);
}
/**
* @return the internal width of the object according to its children.
*/

View File

@@ -16,6 +16,7 @@ namespace gdjs {
_parent: gdjs.RuntimeInstanceContainer;
/** The object that is built with the instances of this container. */
_customObject: gdjs.CustomRuntimeObject;
// TODO Remove this attribute
_isLoaded: boolean = false;
/**
* The default size defined by users in the custom object initial instances editor.
@@ -46,15 +47,28 @@ namespace gdjs {
this._debuggerRenderer = new gdjs.DebuggerRenderer(this);
}
// TODO `_layers` and `_orderedLayers` should not be used directly.
addLayer(layerData: LayerData) {
if (this._layers.containsKey(layerData.name)) {
return;
}
// This code is duplicated with `RuntimeScene.addLayer` because it avoids
// to expose a method to build a layer.
const layer = new gdjs.RuntimeCustomObjectLayer(layerData, this);
this._layers.put(layerData.name, layer);
this._orderedLayers.push(layer);
}
_unloadContent() {
this.onDeletedFromScene(this._parent);
// At this point, layer renderers are already removed by
// `CustomRuntimeObject._reinitializeRenderer`.
// It's not great to do this here, but it allows to keep it private.
this._layers.clear();
this._orderedLayers.length = 0;
}
createObject(objectName: string): gdjs.RuntimeObject | null {
const result = super.createObject(objectName);
this._customObject.onChildrenLocationChanged();
@@ -63,21 +77,14 @@ namespace gdjs {
/**
* Load the container from the given initial configuration.
* @param customObjectData An object containing the container data.
* @param customObjectData An object containing the parent object data.
* @param eventsBasedObjectVariantData An object containing the container data.
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
*/
loadFrom(
customObjectData: ObjectData & CustomObjectConfiguration,
eventsBasedObjectVariantData: EventsBasedObjectVariantData
) {
if (this._isLoaded) {
this.onDeletedFromScene(this._parent);
}
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
!eventsBasedObjectVariantData.name &&
eventsBasedObjectVariantData.instances.length == 0;
this._setOriginalInnerArea(eventsBasedObjectVariantData);
// Registering objects
@@ -87,19 +94,21 @@ namespace gdjs {
++i
) {
const childObjectData = eventsBasedObjectVariantData.objects[i];
// The children configuration override only applies to the default variant.
if (
customObjectData.childrenContent &&
(!eventsBasedObjectVariantData.name ||
isForcedToOverrideEventsBasedObjectChildrenConfiguration)
gdjs.CustomRuntimeObjectInstanceContainer.hasChildrenConfigurationOverriding(
customObjectData,
eventsBasedObjectVariantData
)
) {
this.registerObject({
...childObjectData,
// The custom object overrides its events-based object configuration.
// The custom object overrides its variant configuration with
// a legacy children configuration.
...customObjectData.childrenContent[childObjectData.name],
});
} else {
// The custom object follows its events-based object configuration.
// The custom object follows its variant configuration.
this.registerObject(childObjectData);
}
}
@@ -154,6 +163,28 @@ namespace gdjs {
this._isLoaded = true;
}
/**
* Check if the custom object has a children configuration overriding that
* should be used instead of the variant's objects configurations.
* @param customObjectData An object containing the parent object data.
* @param eventsBasedObjectVariantData An object containing the container data.
* @returns
*/
static hasChildrenConfigurationOverriding(
customObjectData: CustomObjectConfiguration,
eventsBasedObjectVariantData: EventsBasedObjectVariantData
): boolean {
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
!eventsBasedObjectVariantData.name &&
eventsBasedObjectVariantData.instances.length == 0;
// The children configuration override only applies to the default variant.
return customObjectData.childrenContent
? !eventsBasedObjectVariantData.name ||
isForcedToOverrideEventsBasedObjectChildrenConfiguration
: false;
}
/**
* Initialize `_initialInnerArea` if it doesn't exist.
* `_initialInnerArea` is shared by every instance to save memory.
@@ -161,7 +192,10 @@ namespace gdjs {
private _setOriginalInnerArea(
eventsBasedObjectData: EventsBasedObjectVariantData
) {
if (eventsBasedObjectData.instances.length > 0) {
if (
eventsBasedObjectData.instances.length > 0 ||
this.getGame().isInGameEdition()
) {
if (!eventsBasedObjectData._initialInnerArea) {
eventsBasedObjectData._initialInnerArea = {
min: [
@@ -341,6 +375,12 @@ namespace gdjs {
return this._initialInnerArea ? this._initialInnerArea.max[1] : 0;
}
_getInitialInnerAreaDepth(): float {
return this._initialInnerArea
? this._initialInnerArea.max[2] - this._initialInnerArea.min[2]
: 0;
}
getViewportWidth(): float {
return this._customObject.getUnscaledWidth();
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -164,14 +164,17 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
const loadedThreeModel = this._loadedThreeModels.getFromName(
resourceData.name
);
if (loadedThreeModel) {
loadedThreeModel.scene.clear();
this._loadedThreeModels.delete(resourceData);
}
const downloadedArrayBuffer =
this._downloadedArrayBuffers.get(resourceData);
const downloadedArrayBuffer = this._downloadedArrayBuffers.getFromName(
resourceData.name
);
if (downloadedArrayBuffer) {
this._downloadedArrayBuffers.delete(resourceData);
}

View File

@@ -282,6 +282,27 @@ namespace gdjs {
}
}
async loadResources(
resourceNames: Array<string>,
onProgress: (loadingCount: integer, totalCount: integer) => void
): Promise<void> {
let loadedCount = 0;
await processAndRetryIfNeededWithPromisePool(
resourceNames,
maxForegroundConcurrency,
maxAttempt,
async (resourceName) => {
const resource = this._resources.get(resourceName);
if (resource) {
await this._loadResource(resource);
await this._processResource(resource);
}
loadedCount++;
onProgress(loadedCount, this._resources.size);
}
);
}
/**
* Load the resources that are needed to launch the first scene.
*/
@@ -551,6 +572,23 @@ namespace gdjs {
// TODO: mark the scene as unloaded so it's not automatically loaded again eagerly.
}
/**
* To be called when hot-reloading resources.
*/
unloadAllResources(): void {
debugLogger.log(`Unloading of all resources was requested.`);
for (const resource of this._resources.values()) {
const resourceManager = this._resourceManagersMap.get(resource.kind);
if (resourceManager) {
resourceManager.unloadResource(resource);
}
}
for (const sceneLoadingState of this._sceneLoadingStates.values()) {
sceneLoadingState.status = 'not-loaded';
}
debugLogger.log(`Unloading of all resources finished.`);
}
/**
* Put a given scene at the end of the queue.
*
@@ -652,6 +690,9 @@ namespace gdjs {
* the resource (this can be for example a token needed to access the resource).
*/
getFullUrl(url: string) {
if (this._runtimeGame.isInGameEdition()) {
url = addSearchParameterToUrl(url, 'cache', '' + Date.now());
}
const { gdevelopResourceToken } = this._runtimeGame._options;
if (!gdevelopResourceToken) return url;

View File

@@ -312,15 +312,15 @@ namespace gdjs {
xPos: float,
yPos: float,
zPos: float,
trackByPersistentUuid: boolean,
options?: {
excludedObjectNames?: Set<string> | null;
}
trackByPersistentUuid: boolean
): void {
let zOffset: number = zPos;
let shouldTrackByPersistentUuid: boolean = trackByPersistentUuid;
let zOffset: number;
let shouldTrackByPersistentUuid: boolean;
if (arguments.length <= 4) {
if (arguments.length === 5) {
zOffset = zPos;
shouldTrackByPersistentUuid = trackByPersistentUuid;
} else {
/**
* Support for the previous signature (before 3D was introduced):
* createObjectsFrom(data, xPos, yPos, trackByPersistentUuid)
@@ -332,10 +332,6 @@ namespace gdjs {
for (let i = 0, len = data.length; i < len; ++i) {
const instanceData = data[i];
const objectName = instanceData.name;
if (options?.excludedObjectNames?.has(objectName)) {
continue;
}
const newObject = this.createObject(objectName);
if (newObject !== null) {
if (shouldTrackByPersistentUuid) {
@@ -579,6 +575,14 @@ namespace gdjs {
this._cacheOrClearRemovedInstances();
}
_updateObjectsForInGameEditor() {
const allInstancesList = this.getAdhocListOfAllInstances();
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const obj = allInstancesList[i];
obj.update(this);
}
}
/**
* Call each behavior stepPostEvents method.
*/
@@ -613,10 +617,10 @@ namespace gdjs {
* @param name Name of the object for which the instances must be returned.
* @return The list of objects with the given name
*/
getObjects(name: string): gdjs.RuntimeObject[] {
getObjects(name: string): gdjs.RuntimeObject[] | undefined {
if (!this._instances.containsKey(name)) {
logger.info(
'RuntimeScene.getObjects: No instances called "' +
'RuntimeInstanceContainer.getObjects: No instances called "' +
name +
'"! Adding it.'
);
@@ -640,20 +644,18 @@ namespace gdjs {
return null;
}
const objectData = this._objects.get(objectName);
// Create a new object using the object constructor (cached during loading)
// and the stored object's data:
const cache = this._instancesCache.get(objectName);
const ctor = this._objectsCtor.get(objectName);
let obj;
if (!cache || cache.length === 0) {
obj = new ctor(this, objectData);
obj = new ctor(this, this._objects.get(objectName));
} else {
// Reuse an objet destroyed before. If there is an object in the cache,
// then it means it does support reinitialization.
obj = cache.pop();
obj.reinitialize(objectData);
obj.reinitialize(this._objects.get(objectName));
}
this.addObject(obj);
return obj;

View File

@@ -55,13 +55,14 @@ namespace gdjs {
_timeScale: float = 1;
_defaultZOrder: integer = 0;
_hidden: boolean;
_initialEffectsData: Array<EffectData>;
_initialLayerData: LayerData;
// TODO EBO Don't store scene layer related data in layers used by custom objects.
// (both these 3D settings and the lighting layer properties below).
_initialCamera3DFieldOfView: float;
_initialCamera3DFarPlaneDistance: float;
_initialCamera3DNearPlaneDistance: float;
_initialCamera2DPlaneMaxDrawingDistance: float;
_runtimeScene: gdjs.RuntimeInstanceContainer;
_effectsManager: gdjs.EffectsManager;
@@ -94,7 +95,9 @@ namespace gdjs {
layerData.camera3DNearPlaneDistance || 0.1;
this._initialCamera3DFarPlaneDistance =
layerData.camera3DFarPlaneDistance || 2000;
this._initialEffectsData = layerData.effects || [];
this._initialCamera2DPlaneMaxDrawingDistance =
layerData.camera2DPlaneMaxDrawingDistance || 5000;
this._initialLayerData = layerData;
this._runtimeScene = instanceContainer;
this._effectsManager = instanceContainer.getGame().getEffectsManager();
this._isLightingLayer = layerData.isLightingLayer;
@@ -491,6 +494,9 @@ namespace gdjs {
getInitialCamera3DFarPlaneDistance(): float {
return this._initialCamera3DFarPlaneDistance;
}
getInitialCamera2DPlaneMaxDrawingDistance(): float {
return this._initialCamera2DPlaneMaxDrawingDistance;
}
/**
* Return the initial effects data for the layer. Only to
@@ -498,7 +504,7 @@ namespace gdjs {
* @deprecated
*/
getInitialEffectsData(): EffectData[] {
return this._initialEffectsData;
return this._initialLayerData.effects || [];
}
/**

View File

@@ -107,8 +107,12 @@ namespace gdjs {
exception: Error,
runtimeGame: gdjs.RuntimeGame
) => {
const sceneNames = runtimeGame.getSceneStack().getAllSceneNames();
const currentScene = runtimeGame.getSceneStack().getCurrentScene();
const currentScene = runtimeGame.isInGameEdition()
? runtimeGame.getInGameEditor()?.getCurrentScene()
: runtimeGame.getSceneStack().getCurrentScene();
const sceneNames = runtimeGame.isInGameEdition()
? [currentScene?.getName()]
: runtimeGame.getSceneStack().getAllSceneNames();
return {
type: 'javascript-uncaught-exception',
exception,
@@ -116,6 +120,7 @@ namespace gdjs {
playerId: runtimeGame.getPlayerId(),
sessionId: runtimeGame.getSessionId(),
isPreview: runtimeGame.isPreview(),
isInGameEdition: runtimeGame.isInGameEdition(),
gdevelop: {
previewContext: runtimeGame.getAdditionalOptions().previewContext,
isNativeMobileApp: runtimeGame.getAdditionalOptions().nativeMobileApp,
@@ -233,42 +238,250 @@ namespace gdjs {
protected handleCommand(data: any) {
const that = this;
const runtimeGame = this._runtimegame;
const inGameEditor = runtimeGame.getInGameEditor();
if (!data || !data.command) {
// Not a command that's meant to be handled by the debugger, return silently to
// avoid polluting the console.
return;
}
if (data.command === 'play') {
runtimeGame.pause(false);
} else if (data.command === 'pause') {
runtimeGame.pause(true);
that.sendRuntimeGameDump();
} else if (data.command === 'refresh') {
that.sendRuntimeGameDump();
} else if (data.command === 'set') {
that.set(data.path, data.newValue);
} else if (data.command === 'call') {
that.call(data.path, data.args);
} else if (data.command === 'profiler.start') {
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
that.sendProfilerOutput(
stoppedProfiler.getFramesAverageMeasures(),
stoppedProfiler.getStats()
try {
if (data.command === 'play') {
runtimeGame.pause(false);
} else if (data.command === 'pause') {
runtimeGame.pause(true);
that.sendRuntimeGameDump();
} else if (data.command === 'refresh') {
that.sendRuntimeGameDump();
} else if (data.command === 'getStatus') {
that.sendRuntimeGameStatus();
} else if (data.command === 'set') {
that.set(data.path, data.newValue);
} else if (data.command === 'call') {
that.call(data.path, data.args);
} else if (data.command === 'profiler.start') {
runtimeGame.startCurrentSceneProfiler(function (stoppedProfiler) {
that.sendProfilerOutput(
stoppedProfiler.getFramesAverageMeasures(),
stoppedProfiler.getStats()
);
that.sendProfilerStopped();
});
that.sendProfilerStarted();
} else if (data.command === 'profiler.stop') {
runtimeGame.stopCurrentSceneProfiler();
} else if (data.command === 'hotReload') {
const runtimeGameOptions: RuntimeGameOptions =
data.payload.runtimeGameOptions;
if (
(runtimeGameOptions.initialRuntimeGameStatus?.isInGameEdition ||
false) === runtimeGame.isInGameEdition()
) {
this._hasLoggedUncaughtException = false;
that._hotReloader
.hotReload({
projectData: data.payload.projectData,
runtimeGameOptions,
shouldReloadResources:
data.payload.shouldReloadResources || false,
})
.then((logs) => {
that.sendHotReloaderLogs(logs);
});
}
} else if (data.command === 'hotReloadObjects') {
if (inGameEditor) {
const editedInstanceContainer =
inGameEditor.getEditedInstanceContainer();
if (editedInstanceContainer) {
that._hotReloader.hotReloadRuntimeSceneObjects(
data.payload.updatedObjects,
editedInstanceContainer
);
}
}
} else if (data.command === 'hotReloadLayers') {
if (inGameEditor) {
const editedInstanceContainer =
inGameEditor.getEditedInstanceContainer();
if (editedInstanceContainer) {
inGameEditor.onLayersDataChange(
data.payload.layers,
data.payload.areEffectsHidden
);
runtimeGame.getGameData().areEffectsHiddenInEditor =
data.payload.areEffectsHidden;
that._hotReloader.hotReloadRuntimeSceneLayers(
data.payload.layers,
editedInstanceContainer
);
}
}
} else if (data.command === 'setBackgroundColor') {
if (inGameEditor) {
const editedInstanceContainer =
inGameEditor.getEditedInstanceContainer();
if (editedInstanceContainer) {
const backgroundColor = data.payload.backgroundColor;
if (
backgroundColor &&
editedInstanceContainer instanceof gdjs.RuntimeScene
) {
const sceneData = runtimeGame.getSceneData(
editedInstanceContainer.getScene().getName()
);
if (sceneData) {
editedInstanceContainer._backgroundColor =
gdjs.rgbToHexNumber(
backgroundColor[0],
backgroundColor[1],
backgroundColor[2]
);
sceneData.r = backgroundColor[0];
sceneData.v = backgroundColor[1];
sceneData.b = backgroundColor[2];
}
}
}
}
} else if (data.command === 'hotReloadAllInstances') {
if (inGameEditor) {
const editedInstanceContainer =
inGameEditor.getEditedInstanceContainer();
if (editedInstanceContainer) {
that._hotReloader.hotReloadRuntimeInstances(
inGameEditor.getEditedInstanceDataList(),
data.payload.instances,
editedInstanceContainer
);
}
}
} else if (data.command === 'switchForInGameEdition') {
if (!this._runtimegame.isInGameEdition()) return;
const sceneName = data.sceneName || null;
const eventsBasedObjectType = data.eventsBasedObjectType || null;
if (!sceneName && !eventsBasedObjectType) {
logger.warn(
'No scene name specified, switchForInGameEdition aborted'
);
return;
}
if (inGameEditor) {
const wasPaused = this._runtimegame.isPaused();
this._runtimegame.pause(true);
inGameEditor.switchToSceneOrVariant(
data.editorId || null,
sceneName,
data.externalLayoutName || null,
eventsBasedObjectType,
data.eventsBasedObjectVariantName || null,
data.editorCamera3D || null
);
this._runtimegame.pause(wasPaused);
}
} else if (data.command === 'setVisibleStatus') {
if (inGameEditor) {
inGameEditor.setVisibleStatus(data.visible);
}
} else if (data.command === 'updateInstances') {
if (inGameEditor) {
inGameEditor.reloadInstances(data.payload.instances);
}
} else if (data.command === 'addInstances') {
if (inGameEditor) {
inGameEditor.addInstances(data.payload.instances);
inGameEditor.setSelectedObjects(
data.payload.instances.map((instance) => instance.persistentUuid)
);
if (data.payload.moveUnderCursor) {
inGameEditor.moveSelectionUnderCursor();
}
}
} else if (data.command === 'deleteSelection') {
if (inGameEditor) {
inGameEditor.deleteSelection();
}
} else if (data.command === 'dragNewInstance') {
const gameCoords = runtimeGame
.getRenderer()
.convertPageToGameCoords(data.x, data.y);
runtimeGame
.getInputManager()
.onMouseMove(gameCoords[0], gameCoords[1]);
if (inGameEditor)
inGameEditor.dragNewInstance({
name: data.name,
dropped: data.dropped,
isAltPressed: data.isAltPressed,
});
} else if (data.command === 'cancelDragNewInstance') {
if (inGameEditor) inGameEditor.cancelDragNewInstance();
} else if (data.command === 'setInstancesEditorSettings') {
if (inGameEditor)
inGameEditor.updateInstancesEditorSettings(
data.payload.instancesEditorSettings
);
} else if (data.command === 'zoomToInitialPosition') {
if (inGameEditor) {
inGameEditor.zoomToInitialPosition(data.payload.visibleScreenArea);
}
} else if (data.command === 'zoomToFitContent') {
if (inGameEditor) {
inGameEditor.zoomToFitContent(data.payload.visibleScreenArea);
}
} else if (data.command === 'setSelectedLayer') {
if (inGameEditor) {
inGameEditor.setSelectedLayerName(data.payload.layerName);
}
} else if (data.command === 'zoomToFitSelection') {
if (inGameEditor) {
inGameEditor.zoomToFitSelection(data.payload.visibleScreenArea);
}
} else if (data.command === 'zoomBy') {
if (inGameEditor) {
inGameEditor.zoomBy(data.payload.zoomFactor);
}
} else if (data.command === 'setZoom') {
if (inGameEditor) {
inGameEditor.setZoom(data.payload.zoom);
}
} else if (data.command === 'setSelectedInstances') {
if (inGameEditor) {
inGameEditor.setSelectedObjects(data.payload.instanceUuids);
}
} else if (data.command === 'centerViewOnLastSelectedInstance') {
if (inGameEditor) {
// TODO: use data.payload.visibleScreenArea
inGameEditor.centerViewOnLastSelectedInstance();
}
} else if (data.command === 'updateInnerArea') {
if (inGameEditor) {
inGameEditor.updateInnerArea(
data.payload.areaMinX,
data.payload.areaMinY,
data.payload.areaMinZ,
data.payload.areaMaxX,
data.payload.areaMaxY,
data.payload.areaMaxZ
);
}
} else if (data.command === 'getSelectionAABB') {
if (inGameEditor) {
this.sendSelectionAABB(data.messageId);
}
} else if (data.command === 'hardReload') {
// This usually means that the preview was modified so much that an entire reload
// is needed, or that the runtime itself could have been modified.
this.launchHardReload();
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
);
that.sendProfilerStopped();
});
that.sendProfilerStarted();
} else if (data.command === 'profiler.stop') {
runtimeGame.stopCurrentSceneProfiler();
} else if (data.command === 'hotReload') {
that._hotReloader.hotReload().then((logs) => {
that.sendHotReloaderLogs(logs);
});
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
);
}
} catch (error) {
this.onUncaughtException(error);
}
}
@@ -330,9 +543,12 @@ namespace gdjs {
}
onUncaughtException(exception: Error): void {
logger.error('Uncaught exception: ' + exception);
logger.error('Uncaught exception: ', exception, exception.stack);
this._inGameDebugger.setUncaughtException(exception);
const runtimeGame = this._runtimegame;
if (!runtimeGame.isInGameEdition()) {
this._inGameDebugger.setUncaughtException(exception);
}
if (!this._hasLoggedUncaughtException) {
// Only log an uncaught exception once, to avoid spamming the debugger server
@@ -435,6 +651,20 @@ namespace gdjs {
return true;
}
sendRuntimeGameStatus(): void {
const currentScene = this._runtimegame.getSceneStack().getCurrentScene();
this._sendMessage(
circularSafeStringify({
command: 'status',
payload: {
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
sceneName: currentScene ? currentScene.getName() : null,
},
})
);
}
/**
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
*/
@@ -515,7 +745,10 @@ namespace gdjs {
this._sendMessage(
circularSafeStringify({
command: 'hotReloader.logs',
payload: logs,
payload: {
isInGameEdition: this._runtimegame.isInGameEdition(),
logs,
},
})
);
}
@@ -544,26 +777,152 @@ namespace gdjs {
);
}
/**
* Callback called when the game is paused.
*/
sendGamePaused(): void {
sendInstanceChanges(changes: {
isSendingBackSelectionForDefaultSize: boolean;
updatedInstances: Array<InstanceData>;
addedInstances: Array<InstanceData>;
selectedInstances: Array<InstancePersistentUuidData>;
removedInstances: Array<InstancePersistentUuidData>;
}): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'game.paused',
payload: null,
command: 'updateInstances',
editorId: inGameEditor.getEditorId(),
payload: changes,
})
);
}
/**
* Callback called when the game is resumed.
*/
sendGameResumed(): void {
sendOpenContextMenu(cursorX: float, cursorY: float): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'game.resumed',
payload: null,
command: 'openContextMenu',
editorId: inGameEditor.getEditorId(),
payload: { cursorX, cursorY },
})
);
}
sendCameraState(cameraState: EditorCameraState): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'setCameraState',
editorId: inGameEditor.getEditorId(),
payload: cameraState,
})
);
}
sendUndo(): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'undo',
editorId: inGameEditor.getEditorId(),
payload: {},
})
);
}
sendRedo(): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'redo',
editorId: inGameEditor.getEditorId(),
payload: {},
})
);
}
sendCopy(): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'copy',
editorId: inGameEditor.getEditorId(),
payload: {},
})
);
}
sendPaste(): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'paste',
editorId: inGameEditor.getEditorId(),
payload: {},
})
);
}
sendCut(): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
this._sendMessage(
circularSafeStringify({
command: 'cut',
editorId: inGameEditor.getEditorId(),
payload: {},
})
);
}
sendSelectionAABB(messageId: number): void {
const inGameEditor = this._runtimegame.getInGameEditor();
if (!inGameEditor) {
return;
}
const selectionAABB = inGameEditor.getSelectionAABB();
this._sendMessage(
circularSafeStringify({
command: 'selectionAABB',
editorId: inGameEditor.getEditorId(),
messageId,
payload: selectionAABB
? {
minX: selectionAABB.min[0],
minY: selectionAABB.min[1],
minZ: selectionAABB.min[2],
maxX: selectionAABB.max[0],
maxY: selectionAABB.max[1],
maxZ: selectionAABB.max[2],
}
: {
minX: 0,
minY: 0,
minZ: 0,
maxX: 0,
maxY: 0,
maxZ: 0,
},
})
);
}
@@ -587,5 +946,43 @@ namespace gdjs {
})
);
}
launchHardReload(): void {
try {
const reloadUrl = new URL(location.href);
// Construct the initial status to be restored.
const initialRuntimeGameStatus =
this._runtimegame.getAdditionalOptions().initialRuntimeGameStatus;
// We use empty strings to avoid `null` to become `"null"`.
const runtimeGameStatus: RuntimeGameStatus = {
editorId: initialRuntimeGameStatus?.editorId || '',
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
sceneName: initialRuntimeGameStatus?.sceneName || '',
injectedExternalLayoutName:
initialRuntimeGameStatus?.injectedExternalLayoutName || '',
skipCreatingInstancesFromScene:
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
eventsBasedObjectType:
initialRuntimeGameStatus?.eventsBasedObjectType || '',
eventsBasedObjectVariantName:
initialRuntimeGameStatus?.eventsBasedObjectVariantName || '',
editorCamera3D: this._runtimegame.getInGameEditor()?.getCameraState(),
};
reloadUrl.searchParams.set(
'runtimeGameStatus',
JSON.stringify(runtimeGameStatus)
);
location.replace(reloadUrl);
} catch (error) {
logger.error(
'Could not reload the game with the new initial status',
error
);
location.reload();
}
}
}
}

View File

@@ -144,18 +144,30 @@ namespace gdjs {
});
}
hotReload(): Promise<HotReloaderLog[]> {
async hotReload({
shouldReloadResources,
projectData: newProjectData,
runtimeGameOptions: newRuntimeGameOptions,
}: {
shouldReloadResources: boolean;
projectData: ProjectData;
runtimeGameOptions: RuntimeGameOptions;
}): Promise<HotReloaderLog[]> {
logger.info('Hot reload started');
const wasPaused = this._runtimeGame.isPaused();
this._runtimeGame.pause(true);
this._logs = [];
// Save old data of the project, to be used to compute
// the difference between the old and new project data:
const oldProjectData: ProjectData = gdjs.projectData;
gdjs.projectData = newProjectData;
const oldScriptFiles = gdjs.runtimeGameOptions
.scriptFiles as RuntimeGameOptionsScriptFile[];
const oldRuntimeGameOptions = gdjs.runtimeGameOptions;
gdjs.runtimeGameOptions = newRuntimeGameOptions;
const oldScriptFiles =
oldRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
oldScriptFiles.forEach((scriptFile) => {
this._alreadyLoadedScriptFiles[scriptFile.path] = true;
@@ -167,76 +179,102 @@ namespace gdjs {
gdjs.behaviorsTypes.items[behaviorTypeName];
}
// Reload projectData and runtimeGameOptions stored by convention in data.js:
return this._reloadScript('data.js').then(() => {
const newProjectData: ProjectData = gdjs.projectData;
if (gdjs.inAppTutorialMessage) {
gdjs.inAppTutorialMessage.displayInAppTutorialMessage(
this._runtimeGame,
newRuntimeGameOptions.inAppTutorialMessageInPreview,
newRuntimeGameOptions.inAppTutorialMessagePositionInPreview || ''
);
}
const newRuntimeGameOptions: RuntimeGameOptions =
gdjs.runtimeGameOptions;
const newScriptFiles =
newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
const shouldGenerateScenesEventsCode =
!!newRuntimeGameOptions.shouldGenerateScenesEventsCode;
const shouldReloadLibraries =
!!newRuntimeGameOptions.shouldReloadLibraries;
if (gdjs.inAppTutorialMessage) {
gdjs.inAppTutorialMessage.displayInAppTutorialMessage(
this._runtimeGame,
newRuntimeGameOptions.inAppTutorialMessageInPreview,
newRuntimeGameOptions.inAppTutorialMessagePositionInPreview || ''
// Reload the changed scripts, which will have the side effects of re-running
// the new scripts, potentially replacing the code of the free functions from
// extensions (which is fine) and registering updated behaviors (which will
// need to be re-instantiated in runtime objects).
try {
if (shouldReloadLibraries) {
await this.reloadScriptFiles(
newProjectData,
oldScriptFiles,
newScriptFiles,
shouldGenerateScenesEventsCode
);
}
const newScriptFiles =
newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
const projectDataOnlyExport =
!!newRuntimeGameOptions.projectDataOnlyExport;
// Reload the changed scripts, which will have the side effects of re-running
// the new scripts, potentially replacing the code of the free functions from
// extensions (which is fine) and registering updated behaviors (which will
// need to be re-instantiated in runtime objects).
return this.reloadScriptFiles(
newProjectData,
oldScriptFiles,
newScriptFiles,
projectDataOnlyExport
)
.then(() => {
const changedRuntimeBehaviors =
this._computeChangedRuntimeBehaviors(
oldBehaviorConstructors,
gdjs.behaviorsTypes.items
);
return this._hotReloadRuntimeGame(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
this._runtimeGame
const newRuntimeGameStatus =
newRuntimeGameOptions.initialRuntimeGameStatus;
if (
newRuntimeGameStatus &&
newRuntimeGameStatus.editorId &&
newRuntimeGameStatus.isInGameEdition
) {
if (shouldReloadResources) {
// Unloading all resources will force them to be loaded again,
// which is sufficient for ensuring they are up-to-date as
// resources will be loaded with a 'cache bursting' parameter.
this._runtimeGame._resourcesLoader.unloadAllResources();
}
// The editor don't need to hot-reload the current scene because the
// editor always stays in the initial state.
this._runtimeGame.setProjectData(newProjectData);
await this._runtimeGame.loadFirstAssetsAndStartBackgroundLoading(
newRuntimeGameStatus.sceneName || newProjectData.firstLayout,
() => {}
);
const inGameEditor = this._runtimeGame.getInGameEditor();
if (inGameEditor) {
await inGameEditor.switchToSceneOrVariant(
newRuntimeGameStatus.editorId || null,
newRuntimeGameStatus.sceneName,
newRuntimeGameStatus.injectedExternalLayoutName,
newRuntimeGameStatus.eventsBasedObjectType,
newRuntimeGameStatus.eventsBasedObjectVariantName,
newRuntimeGameStatus.editorCamera3D || null
);
})
.catch((error) => {
const errorTarget = error.target;
if (errorTarget instanceof HTMLScriptElement) {
this._logs.push({
kind: 'fatal',
message: 'Unable to reload script: ' + errorTarget.src,
});
} else {
this._logs.push({
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading: ' +
error.message +
'\n' +
error.stack,
});
}
})
.then(() => {
logger.info(
'Hot reload finished with logs:',
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
);
this._runtimeGame.pause(false);
return this._logs;
}
} else {
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
oldBehaviorConstructors,
gdjs.behaviorsTypes.items
);
await this._hotReloadRuntimeGame(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
this._runtimeGame
);
}
} catch (error) {
const errorTarget = error.target;
if (errorTarget instanceof HTMLScriptElement) {
this._logs.push({
kind: 'fatal',
message: 'Unable to reload script: ' + errorTarget.src,
});
});
} else {
this._logs.push({
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading: ' +
error.message +
'\n' +
error.stack,
});
}
}
logger.info(
'Hot reload finished with logs:',
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
);
this._runtimeGame.pause(wasPaused);
return this._logs;
}
_computeChangedRuntimeBehaviors(
@@ -281,12 +319,12 @@ namespace gdjs {
newProjectData: ProjectData,
oldScriptFiles: RuntimeGameOptionsScriptFile[],
newScriptFiles: RuntimeGameOptionsScriptFile[],
projectDataOnlyExport: boolean
shouldGenerateScenesEventsCode: boolean
): Promise<void[]> {
const reloadPromises: Array<Promise<void>> = [];
// Reload events, only if they were exported.
if (!projectDataOnlyExport) {
if (shouldGenerateScenesEventsCode) {
newProjectData.layouts.forEach((_layoutData, index) => {
reloadPromises.push(this._reloadScript('code' + index + '.js'));
});
@@ -326,7 +364,7 @@ namespace gdjs {
)[0];
// A file may be removed because of a partial preview.
if (!newScriptFile && !projectDataOnlyExport) {
if (!newScriptFile && !shouldGenerateScenesEventsCode) {
this._logs.push({
kind: 'warning',
message: 'Script file ' + oldScriptFile.path + ' was removed.',
@@ -694,6 +732,16 @@ namespace gdjs {
runtimeScene.setEventsGeneratedCodeFunction(newLayoutData);
}
/**
* Add the children object data into every custom object data.
*
* At the runtime, this is done at the object instantiation.
* For hot-reloading, it's done before hands to optimize.
*
* @param projectData The project data
* @param objectDatas The object datas to modify
* @returns
*/
static resolveCustomObjectConfigurations(
projectData: ProjectData,
objectDatas: ObjectData[]
@@ -717,27 +765,43 @@ namespace gdjs {
if (!eventsBasedObjectData) {
return objectData;
}
const customObjectConfiguration = objectData as ObjectData &
CustomObjectConfiguration;
const eventsBasedObjectVariantData =
gdjs.RuntimeGame._getEventsBasedObjectVariantData(
eventsBasedObjectData,
customObjectConfiguration.variant
);
// Apply the legacy children configuration overriding if any.
const mergedChildObjectDataList =
customObjectConfiguration.childrenContent
? eventsBasedObjectData.objects.map((objectData) => ({
...objectData,
...customObjectConfiguration.childrenContent[objectData.name],
}))
gdjs.CustomRuntimeObjectInstanceContainer.hasChildrenConfigurationOverriding(
customObjectConfiguration,
eventsBasedObjectVariantData
)
? eventsBasedObjectData.objects.map((objectData) =>
customObjectConfiguration.childrenContent
? {
...objectData,
...customObjectConfiguration.childrenContent[
objectData.name
],
}
: objectData
)
: eventsBasedObjectData.objects;
const mergedObjectConfiguration = {
...eventsBasedObjectData,
...objectData,
// ObjectData doesn't have an `objects` attribute.
// ObjectData doesn't have an `objects` nor `instances` attribute.
// This is a small optimization to avoid to create an
// InstanceContainerData for each instance to hot-reload their inner
// scene (see `_hotReloadRuntimeInstanceContainer` call from
// `_hotReloadRuntimeSceneInstances`).
...eventsBasedObjectData,
...eventsBasedObjectVariantData,
objects: mergedChildObjectDataList,
// It must be the last one to ensure the object name won't be overridden.
...objectData,
};
return mergedObjectConfiguration;
});
@@ -751,6 +815,12 @@ namespace gdjs {
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
if (!oldLayoutData.objects || !newLayoutData.objects) {
// It can happen when `hotReloadRuntimeInstances` is executed.
// `hotReloadRuntimeInstances` doesn't resolve the custom objects
// because it can only modify the 1st level of instances.
return;
}
const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations(
oldProjectData,
oldLayoutData.objects
@@ -921,16 +991,62 @@ namespace gdjs {
return;
}
hotReloadRuntimeSceneObjects(
updatedObjects: Array<ObjectData>,
// runtimeInstanceContainer gives an access as a map.
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
const oldObjects: Array<ObjectData | null> = updatedObjects.map(
(objectData) =>
runtimeInstanceContainer._objects.get(objectData.name) || null
);
const projectData: ProjectData = this._runtimeGame._data;
const newObjectDataList = HotReloader.resolveCustomObjectConfigurations(
projectData,
updatedObjects
);
this._hotReloadRuntimeSceneObjects(
oldObjects,
newObjectDataList,
runtimeInstanceContainer
);
// Update the GameData
for (let index = 0; index < updatedObjects.length; index++) {
const oldObjectData = oldObjects[index];
// When the object is new, the hot-reload call `registerObject`
// so `_objects` is already updated.
if (oldObjectData) {
// In gdjs.CustomRuntimeObjectInstanceContainer.loadFrom, object can
// be registered with a different instance from the ProjectData. This
// is only done for children of a custom object with a children overriding.
// In the case of the editor, the fake custom object used for editing
// variants has no children overriding (see
// gdjs.RuntimeGame._createSceneWithCustomObject).
// Thus, the oldObjectData is always the one from the ProjectData.
HotReloader.assignOrDelete(oldObjectData, updatedObjects[index]);
} else {
console.warn(
`Can't update object data for "${updatedObjects[index].name}" because it doesn't exist.`
);
}
}
}
_hotReloadRuntimeSceneObjects(
oldObjects: ObjectData[],
oldObjects: Array<ObjectData | null>,
newObjects: ObjectData[],
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
oldObjects.forEach((oldObjectData) => {
if (!oldObjectData) {
return;
}
const name = oldObjectData.name;
const newObjectData = newObjects.filter(
const newObjectData = newObjects.find(
(objectData) => objectData.name === name
)[0];
);
// Note: if an object is renamed in the editor, it will be considered as removed,
// and the new object name as a new object to register.
@@ -952,9 +1068,9 @@ namespace gdjs {
});
newObjects.forEach((newObjectData) => {
const name = newObjectData.name;
const oldObjectData = oldObjects.filter(
(layerData) => layerData.name === name
)[0];
const oldObjectData = oldObjects.find(
(layerData) => layerData && layerData.name === name
);
if (
(!oldObjectData || oldObjectData.type !== newObjectData.type) &&
!runtimeInstanceContainer.isObjectRegistered(name)
@@ -1192,6 +1308,31 @@ namespace gdjs {
);
}
hotReloadRuntimeSceneLayers(
newLayers: LayerData[],
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
const layerNames = [];
runtimeInstanceContainer.getAllLayerNames(layerNames);
const oldLayers = layerNames.map((layerName) =>
runtimeInstanceContainer.hasLayer(layerName)
? runtimeInstanceContainer.getLayer(layerName)._initialLayerData
: null
);
this._hotReloadRuntimeSceneLayers(
oldLayers.filter(Boolean) as LayerData[],
newLayers,
runtimeInstanceContainer
);
// Update the GameData
for (let index = 0; index < newLayers.length; index++) {
const oldLayer = oldLayers[index];
if (oldLayer) {
HotReloader.assignOrDelete(oldLayer, newLayers[index]);
}
}
}
_hotReloadRuntimeSceneLayers(
oldLayers: LayerData[],
newLayers: LayerData[],
@@ -1273,6 +1414,8 @@ namespace gdjs {
newLayer.effects,
runtimeLayer
);
runtimeLayer._initialLayerData = newLayer;
}
_hotReloadRuntimeLayerEffects(
@@ -1357,6 +1500,28 @@ namespace gdjs {
}
}
hotReloadRuntimeInstances(
oldInstances: InstanceData[],
newInstances: InstanceData[],
runtimeInstanceContainer: RuntimeInstanceContainer
): void {
const projectData: ProjectData = gdjs.projectData;
const objects: Array<ObjectData> = [];
runtimeInstanceContainer._objects.values(objects);
projectData.layouts;
this._hotReloadRuntimeSceneInstances(
projectData,
projectData,
[],
objects,
objects,
oldInstances,
newInstances,
runtimeInstanceContainer
);
gdjs.copyArray(newInstances, oldInstances);
}
_hotReloadRuntimeSceneInstances(
oldProjectData: ProjectData,
newProjectData: ProjectData,
@@ -1423,6 +1588,9 @@ namespace gdjs {
);
} else {
// Reload objects that were created at runtime.
// This is a subset of what is done by `_hotReloadRuntimeInstance`.
// Since the instance doesn't exist in the editor, it's properties
// can't be updated, only the object changes are applied.
// Update variables
this._hotReloadVariablesContainer(
@@ -1431,6 +1599,7 @@ namespace gdjs {
runtimeObject.getVariables()
);
// Update the content of custom object
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
const childrenInstanceContainer =
runtimeObject.getChildrenContainer();
@@ -1443,15 +1612,18 @@ namespace gdjs {
CustomObjectConfiguration &
InstanceContainerData;
// Reload the content of custom objects that were created at runtime.
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
// Variant swapping is handled by `CustomRuntimeObject.updateFromObjectData`.
if (newCustomObjectData.variant === oldCustomObjectData.variant) {
// Reload the content of custom objects that were created at runtime.
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
}
}
}
}
@@ -1513,22 +1685,16 @@ namespace gdjs {
somethingChanged = true;
}
if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(runtimeObject)) {
if (oldInstance.z !== newInstance.z && newInstance.z !== undefined) {
runtimeObject.setZ(newInstance.z);
if (oldInstance.z !== newInstance.z) {
runtimeObject.setZ(newInstance.z || 0);
somethingChanged = true;
}
if (
oldInstance.rotationX !== newInstance.rotationX &&
newInstance.rotationX !== undefined
) {
runtimeObject.setRotationX(newInstance.rotationX);
if (oldInstance.rotationX !== newInstance.rotationX) {
runtimeObject.setRotationX(newInstance.rotationX || 0);
somethingChanged = true;
}
if (
oldInstance.rotationY !== newInstance.rotationY &&
newInstance.rotationY !== undefined
) {
runtimeObject.setRotationY(newInstance.rotationY);
if (oldInstance.rotationY !== newInstance.rotationY) {
runtimeObject.setRotationY(newInstance.rotationY || 0);
somethingChanged = true;
}
}
@@ -1583,8 +1749,6 @@ namespace gdjs {
}
}
if (runtimeObject instanceof gdjs.CustomRuntimeObject) {
const childrenInstanceContainer = runtimeObject.getChildrenContainer();
// The `objects` attribute is already resolved by `resolveCustomObjectConfigurations()`.
const oldCustomObjectData = oldObjectData as ObjectData &
CustomObjectConfiguration &
@@ -1593,14 +1757,19 @@ namespace gdjs {
CustomObjectConfiguration &
InstanceContainerData;
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
// Variant swapping is handled by `CustomRuntimeObject.updateFromObjectData`.
if (newCustomObjectData.variant === oldCustomObjectData.variant) {
const childrenInstanceContainer =
runtimeObject.getChildrenContainer();
this._hotReloadRuntimeInstanceContainer(
oldProjectData,
newProjectData,
oldCustomObjectData,
newCustomObjectData,
changedRuntimeBehaviors,
childrenInstanceContainer
);
}
}
// Update variables
@@ -1727,5 +1896,23 @@ namespace gdjs {
// true if both NaN, false otherwise
return a !== a && b !== b;
}
static assignOrDelete(
target: any,
source: any,
ignoreKeys: string[] = []
): void {
Object.assign(target, source);
for (const key in target) {
if (ignoreKeys.includes(key)) {
continue;
}
if (Object.prototype.hasOwnProperty.call(target, key)) {
if (source[key] === undefined) {
delete target[key];
}
}
}
}
}
}

View File

@@ -56,6 +56,19 @@ namespace gdjs {
};
this._ws.onclose = function close() {
logger.info('Debugger connection closed');
if (that._runtimegame.isInGameEdition()) {
// Sometimes, for example if the editor is launched for a long time and the device goes to sleep,
// the WebSocket connection between the editor and the game is closed. When we are in in-game edition,
// we can't afford to lose the connection because it means the editor is unusable.
// In this case, we hard reload the game to re-establish a new connection.
setTimeout(() => {
logger.info(
'Debugger connection closed while in in-game edition - this is suspicious so hard reloading to re-establish a new connection.'
);
that.launchHardReload();
}, 1000);
}
};
this._ws.onerror = function errored(error) {
logger.warn('Debugger client error:', error);

View File

@@ -11,7 +11,13 @@ namespace gdjs {
constructor(runtimeGame: RuntimeGame) {
super(runtimeGame);
// Opener is either the `opener` for popups, or the `parent` if the game
// is running as an iframe (notably: in-game edition).
this._opener = window.opener || null;
if (!this._opener && window.parent !== window) {
this._opener = window.parent;
}
if (!this._opener) {
logger.info("`window.opener` not existing, the debugger won't work.");
return;

View File

@@ -207,7 +207,7 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const resource = this._loadedFontFamily.get(resourceData);
const resource = this._loadedFontFamily.getFromName(resourceData.name);
if (resource) {
this._loadedFontFamily.delete(resourceData);
}

View File

@@ -1157,12 +1157,12 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const musicRes = this._loadedMusics.get(resourceData);
const musicRes = this._loadedMusics.getFromName(resourceData.name);
if (musicRes) {
this.unloadAudio(resourceData.name, true);
}
const soundRes = this._loadedSounds.get(resourceData);
const soundRes = this._loadedSounds.getFromName(resourceData.name);
if (soundRes) {
this.unloadAudio(resourceData.name, false);
}

View File

@@ -33,24 +33,28 @@ namespace gdjs {
/**
* The cursor X position (moved by mouse and touch events).
*/
_cursorX: float = 0;
private _cursorX: float = 0;
/**
* The cursor Y position (moved by mouse and touch events).
*/
_cursorY: float = 0;
private _cursorY: float = 0;
/**
* The mouse X position (only moved by mouse events).
*/
_mouseX: float = 0;
private _mouseX: float = 0;
/**
* The mouse Y position (only moved by mouse events).
*/
_mouseY: float = 0;
_isMouseInsideCanvas: boolean = true;
_mouseWheelDelta: float = 0;
private _mouseY: float = 0;
private _isMouseInsideCanvas: boolean = true;
private _wheelDeltaX: float = 0;
private _wheelDeltaY: float = 0;
private _wheelDeltaZ: float = 0;
// TODO Remove _touches when there is no longer SpritePanelButton 1.2.0
// extension in the wild.
_touches = {
// @ts-ignore
private _touches = {
firstKey: (): string | number | null => {
for (const key in this._mouseOrTouches.items) {
// Exclude mouse key.
@@ -61,22 +65,23 @@ namespace gdjs {
return null;
},
};
_mouseOrTouches: Hashtable<Touch>;
private _mouseOrTouches: Hashtable<Touch>;
//Identifiers of the touches that started during/before the frame.
_startedTouches: Array<integer> = [];
private _startedTouches: Array<integer> = [];
//Identifiers of the touches that ended during/before the frame.
_endedTouches: Array<integer> = [];
_touchSimulateMouse: boolean = true;
private _endedTouches: Array<integer> = [];
private _touchSimulateMouse: boolean = true;
/**
* @deprecated
*/
_lastStartedTouchIndex = 0;
private _lastStartedTouchIndex = 0;
/**
* @deprecated
*/
_lastEndedTouchIndex = 0;
private _lastEndedTouchIndex = 0;
constructor() {
this._pressedKeys = new Hashtable();
@@ -96,7 +101,7 @@ namespace gdjs {
* @param keyCode The raw key code
* @param location The location
*/
_getLocationAwareKeyCode(
static getLocationAwareKeyCode(
keyCode: number,
location: number | null | undefined
): integer {
@@ -121,7 +126,7 @@ namespace gdjs {
* @param location The location of the event.
*/
onKeyPressed(keyCode: number, location?: number): void {
const locationAwareKeyCode = this._getLocationAwareKeyCode(
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
keyCode,
location
);
@@ -138,7 +143,7 @@ namespace gdjs {
* @param location The location of the event.
*/
onKeyReleased(keyCode: number, location?: number): void {
const locationAwareKeyCode = this._getLocationAwareKeyCode(
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
keyCode,
location
);
@@ -335,6 +340,19 @@ namespace gdjs {
}
}
/**
* Return true if any mouse button is pressed.
* @return true if any mouse button is pressed.
*/
anyMouseButtonPressed(): boolean {
for (const buttonCode in this._pressedMouseButtons) {
if (this._pressedMouseButtons[buttonCode]) {
return true;
}
}
return false;
}
_setMouseButtonPressed(buttonCode: number): void {
this._pressedMouseButtons[buttonCode] = true;
this._releasedMouseButtons[buttonCode] = false;
@@ -380,17 +398,37 @@ namespace gdjs {
/**
* Should be called whenever the mouse wheel is used
* @param wheelDelta The mouse wheel delta
* @param wheelDeltaY The mouse wheel delta
*/
onMouseWheel(wheelDelta: number): void {
this._mouseWheelDelta = wheelDelta;
onMouseWheel(
wheelDeltaY: number,
wheelDeltaX: number,
wheelDeltaZ: number
): void {
this._wheelDeltaY = wheelDeltaY;
if (wheelDeltaX !== undefined) this._wheelDeltaX = wheelDeltaX;
if (wheelDeltaZ !== undefined) this._wheelDeltaZ = wheelDeltaZ;
}
/**
* Return the mouse wheel delta
* Return the mouse wheel delta on Y axis.
*/
getMouseWheelDelta(): float {
return this._mouseWheelDelta;
return this._wheelDeltaY;
}
/**
* Return the mouse wheel delta on X axis.
*/
getMouseWheelDeltaX(): float {
return this._wheelDeltaX;
}
/**
* Return the mouse wheel delta on Z axis.
*/
getMouseWheelDeltaZ(): float {
return this._wheelDeltaZ;
}
/**
@@ -578,7 +616,9 @@ namespace gdjs {
this._releasedKeys.clear();
this._justPressedKeys.clear();
this._releasedMouseButtons.length = 0;
this._mouseWheelDelta = 0;
this._wheelDeltaX = 0;
this._wheelDeltaY = 0;
this._wheelDeltaZ = 0;
this._lastStartedTouchIndex = 0;
this._lastEndedTouchIndex = 0;
}

View File

@@ -210,12 +210,12 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const loadedJson = this._loadedJsons.get(resourceData);
const loadedJson = this._loadedJsons.getFromName(resourceData.name);
if (loadedJson) {
this._loadedJsons.delete(resourceData);
}
const callback = this._callbacks.get(resourceData);
const callback = this._callbacks.getFromName(resourceData.name);
if (callback) {
this._callbacks.delete(resourceData);
}

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,178 @@
namespace gdjs {
const logger = new gdjs.Logger('LayerPixiRenderer');
const FRUSTUM_EDGES: Array<[number, number]> = [
// near plane edges
[0, 1],
[1, 2],
[2, 3],
[3, 0],
// far plane edges
[4, 5],
[5, 6],
[6, 7],
[7, 4],
// near↔far connections
[0, 4],
[1, 5],
[2, 6],
[3, 7],
];
/** Normalized Device Coordinates corners for near (-1) and far (+1) planes (Three.js NDC: z=-1 near, z=+1 far). */
const NDC_CORNERS: Array<Array<float>> = [
// near
[-1, -1, -1],
[+1, -1, -1],
[+1, +1, -1],
[-1, +1, -1],
// far
[-1, -1, +1],
[+1, -1, +1],
[+1, +1, +1],
[-1, +1, +1],
];
/** Sort convex polygon vertices around centroid to get consistent winding. */
const sortConvexPolygon = (points: THREE.Vector3[]): THREE.Vector3[] => {
if (points.length <= 2) return points;
const cx = points.reduce((s, p) => s + p.x, 0) / points.length;
const cy = points.reduce((s, p) => s + p.y, 0) / points.length;
return points
.map((p) => ({ p, a: Math.atan2(p.y - cy, p.x - cx) }))
.sort((u, v) => u.a - v.a)
.map((u) => u.p);
};
/**
* Intersect a frustum edge segment [a,b] with plane Z=0.
* Returns point or null if no intersection on the segment.
*/
const intersectSegmentWithZ0 = (
a: THREE.Vector3,
b: THREE.Vector3,
eps = 1e-9
): THREE.Vector3 | null => {
const az = a.z,
bz = b.z;
const dz = bz - az;
// If both z on same side and not on plane, no crossing.
if (Math.abs(dz) < eps) {
// Segment is (almost) parallel to plane.
if (Math.abs(az) < eps && Math.abs(bz) < eps) {
// Entire segment lies on plane: return endpoints (handled by caller via dedup).
// Here we return null and let caller add endpoints if needed.
return null;
}
return null;
}
// Solve a.z + t*(b.z - a.z) = 0 ⇒ t = -a.z / (b.z - a.z)
const t = -az / dz;
if (t < -eps || t > 1 + eps) {
// Intersection beyond the segment bounds.
return null;
}
const p = new THREE.Vector3(
a.x + t * (b.x - a.x),
a.y + t * (b.y - a.y),
0
);
return p;
};
/** Remove near-duplicate points. */
const dedupPoints = (
points: THREE.Vector3[],
eps = 1e-6
): THREE.Vector3[] => {
const out: THREE.Vector3[] = [];
for (const p of points) {
const exists = out.some(
(q) => Math.abs(p.x - q.x) < eps && Math.abs(p.y - q.y) < eps
);
if (!exists) out.push(p);
}
return out;
};
/**
* Compute the convex polygon of the camera frustum clipped by plane Z=0.
* Returns ordered vertices (world coords, z=0). Empty array if no intersection.
*/
const clipFrustumAgainstZ0 = (camera: THREE.Camera): THREE.Vector3[] => {
camera.updateMatrixWorld(true);
// Get the 8 corners of the camera frustum in world coordinates.
const corners = NDC_CORNERS.map((ndc) =>
new THREE.Vector3(ndc[0], ndc[1], ndc[2]).unproject(camera)
);
if (corners.length !== 8) return [];
const hits: THREE.Vector3[] = [];
// 1) Add vertices that already lie on the plane (z≈0).
for (const v of corners) {
if (Math.abs(v.z) < 1e-9) {
hits.push(new THREE.Vector3(v.x, v.y, 0));
}
}
// 2) Intersect each frustum edge with plane Z=0.
for (const [i, j] of FRUSTUM_EDGES) {
const a = corners[i],
b = corners[j];
const p = intersectSegmentWithZ0(a, b);
if (p) hits.push(p);
}
// Deduplicate and order.
const unique = dedupPoints(hits);
if (unique.length < 3) return [];
return sortConvexPolygon(unique);
};
/**
* Intersect the ray going through a normalized device coordinate (nx, ny)
* with the plane Z=0. Returns the hit point in THREE world coords (z=0)
* or null if the ray doesn't intersect the plane in front of the camera.
*/
const projectNDCToZ0 = (
camera: THREE.Camera,
nx: number,
ny: number
): THREE.Vector3 | null => {
if (!camera) return null;
camera.updateMatrixWorld(true);
const origin = new THREE.Vector3();
const dir = new THREE.Vector3();
const p = new THREE.Vector3(nx, ny, 0.5);
if (camera instanceof THREE.OrthographicCamera) {
// For ortho, unproject a point on the camera plane, and use forward dir.
p.z = 0; // on the camera plane
p.unproject(camera); // gives a point on the camera plane in world coords
origin.copy(p);
camera.getWorldDirection(dir);
} else {
// Perspective: unproject a point on the frustum plane, build a ray.
p.unproject(camera);
origin.copy(camera.position);
dir.copy(p).sub(origin).normalize();
}
const dz = dir.z;
if (Math.abs(dz) < 1e-8) return null; // parallel
const t = -origin.z / dz;
if (t <= 0) return null; // behind the camera => not visible
return origin.addScaledVector(dir, t).setZ(0);
};
/**
* The renderer for a gdjs.Layer using Pixi.js.
*/
@@ -47,6 +219,7 @@ namespace gdjs {
private _threePlaneGeometry: THREE.PlaneGeometry | null = null;
private _threePlaneMaterial: THREE.ShaderMaterial | null = null;
private _threePlaneMesh: THREE.Mesh | null = null;
private _threePlaneMeshDebugOutline: THREE.LineSegments | null = null;
/**
* Pixi doesn't sort children with zIndex == 0.
@@ -99,6 +272,9 @@ namespace gdjs {
// The layer is now fully initialized. Adapt the 3D camera position
// (which we could not do before in `_setup3DRendering`).
this._update3DCameraAspectAndPosition();
// Uncomment to show the outline of the 2D rendering plane.
// this.show2DRenderingPlaneDebugOutline(true);
}
onGameResolutionResized() {
@@ -134,6 +310,10 @@ namespace gdjs {
return this._threeScene;
}
getThreeGroup(): THREE.Group | null {
return this._threeGroup;
}
getThreeCamera():
| THREE.PerspectiveCamera
| THREE.OrthographicCamera
@@ -285,6 +465,10 @@ namespace gdjs {
'Tried to setup PixiJS plane for 2D rendering in 3D for a layer that is already set up.'
);
this.set2DPlaneMaxDrawingDistance(
this._layer.getInitialCamera2DPlaneMaxDrawingDistance()
);
// If we have both 2D and 3D objects to be rendered, create a render texture that PixiJS will use
// to render, and that will be projected on a plane by Three.js
this._createPixiRenderTexture(pixiRenderer);
@@ -388,30 +572,298 @@ namespace gdjs {
}
/**
* Update the position of the PIXI container. To be called after each change
* made to position, zoom or rotation of the camera.
* Enable or disable the drawing of an outline of the 2D rendering plane.
* Useful to visually see where the 2D rendering is done in the 3D world.
*/
updatePosition(): void {
const angle = -gdjs.toRad(this._layer.getCameraRotation());
const zoomFactor = this._layer.getCameraZoom();
this._pixiContainer.rotation = angle;
this._pixiContainer.scale.x = zoomFactor;
this._pixiContainer.scale.y = zoomFactor;
const cosValue = Math.cos(angle);
const sinValue = Math.sin(angle);
const centerX =
this._layer.getCameraX() * zoomFactor * cosValue -
this._layer.getCameraY() * zoomFactor * sinValue;
const centerY =
this._layer.getCameraX() * zoomFactor * sinValue +
this._layer.getCameraY() * zoomFactor * cosValue;
this._pixiContainer.position.x = this._layer.getWidth() / 2 - centerX;
this._pixiContainer.position.y = this._layer.getHeight() / 2 - centerY;
show2DRenderingPlaneDebugOutline(enable: boolean) {
if (!this._threePlaneMesh) return;
if (enable && !this._threePlaneMeshDebugOutline) {
// Add rectangle outline around the plane.
const edges = new THREE.EdgesGeometry(this._threePlaneGeometry);
const lineMaterial = new THREE.LineBasicMaterial({
color: 0xff0000,
});
this._threePlaneMeshDebugOutline = new THREE.LineSegments(
edges,
lineMaterial
);
// Attach the outline to the plane so it follows position/scale/rotation.
this._threePlaneMesh.add(this._threePlaneMeshDebugOutline);
}
if (!enable && this._threePlaneMeshDebugOutline) {
this._threePlaneMesh.remove(this._threePlaneMeshDebugOutline);
this._threePlaneMeshDebugOutline = null;
}
}
/** Maximum size of the 2D plane, in pixels. */
private _2DPlaneMaxDrawingDistance: number = 5000;
/** Tilt degrees below which the 2D plane is not clamped. */
private _2DPlaneClampFreeTiltDeg: number = 0.1;
/** Tilt degrees below which the 2D plane is fully clamped. */
private _2DPlaneClampHardTiltDeg: number = 6;
private _2DPlaneClampRampPower: number = 1.5; // 1 = linear, >1 = smoother
/**
* Set the maximum "drawing distance", in pixels, of the 2D when in the 3D world.
* This corresponds to the "height" of the 2D plane.
* Used when the 3D camera is tilted on the X or Y axis (instead of looking down the Z axis,
* as it's done by default for 2D games).
* This is useful to avoid the 2D plane being too big when the camera is tilted.
*/
set2DPlaneMaxDrawingDistance(h: number) {
this._2DPlaneMaxDrawingDistance = Math.max(0, h);
}
/**
* Set the tilt degrees below which the 2D plane is not clamped.
*/
set2DPlaneClampFreeTiltDegrees(d: number) {
this._2DPlaneClampFreeTiltDeg = Math.max(0, d);
}
/**
* Set the tilt degrees below which the 2D plane is clamped (see `set2DPlaneMaxDrawingDistance`).
*/
set2DPlaneClampHardTiltDegrees(d: number) {
this._2DPlaneClampHardTiltDeg = Math.max(0, d);
}
/**
* Set the ramp power of the 2D plane clamping (see `set2DPlaneMaxDrawingDistance`). Used
* for smoother transition between clamped and unclamped.
*/
set2DPlaneClampRampPower(p: number) {
this._2DPlaneClampRampPower = Math.max(0.1, p);
}
/**
* Get the size of the 2D plane, in the world coordinates.
*/
private _get2DPlaneSize(): [number, number] {
if (!this._threeCamera) return [0, 0];
// Compute the intersection of the frustrum of the camera on the Z=0 plane.
// In theory, that's where the entire 2D rendering should be displayed.
const poly = clipFrustumAgainstZ0(this._threeCamera);
if (poly.length === 0) {
// No intersection at all: Z=0 not in view.
return [0, 0];
}
// Compute the axis-aligned bounds on Z=0 (world units) of the polygon,
// so we can compute the size of the plane doing the 2D rendering.
let minX = Infinity,
maxX = -Infinity,
minY = Infinity,
maxY = -Infinity;
for (const p of poly) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
let boxW = Math.max(1e-8, maxX - minX);
let boxH = Math.max(1e-8, maxY - minY);
// Keep 2D layer aspect ratio (so texture isn't stretched).
const targetAspect = this._layer.getWidth() / this._layer.getHeight();
const boxAspect = boxW / boxH;
if (boxAspect < targetAspect) {
boxW = targetAspect * boxH;
} else {
boxH = boxW / targetAspect;
}
// Decide if we should cap based on camera tilt (X/Y) ---
const forward = new THREE.Vector3();
this._threeCamera.getWorldDirection(forward);
// |forward.z| ≈ 1 -> no tilt (look mostly perpendicular to Z=0).
// |forward.z| ≈ 0 -> grazing the horizon (strong tilt).
const freeCos = Math.cos(
THREE.MathUtils.degToRad(this._2DPlaneClampFreeTiltDeg)
);
const hardCos = Math.cos(
THREE.MathUtils.degToRad(this._2DPlaneClampHardTiltDeg)
);
const tiltCos = Math.abs(forward.z);
// Map tiltCos ∈ [hardCos, freeCos] to w ∈ [1, 0]
let w = 0;
if (tiltCos <= hardCos)
w = 1; // fully clamped
else if (tiltCos >= freeCos)
w = 0; // no clamp
else w = (freeCos - tiltCos) / (freeCos - hardCos);
// Ease it
w = Math.pow(w, this._2DPlaneClampRampPower);
// Interpolate Infinity→base via 1/w (bounded):
const BIG = 1e12; // “practically infinite”
const denom = Math.max(w, 1e-6);
const effectiveMaxH = Math.min(
BIG,
this._2DPlaneMaxDrawingDistance / denom
);
// Apply the max height.
if (effectiveMaxH < BIG) {
const clampedH = Math.max(1e-8, Math.min(boxH, effectiveMaxH));
if (clampedH !== boxH) {
boxH = clampedH;
boxW = targetAspect * boxH; // keep aspect
}
}
return [boxW, boxH];
}
private _get2DPlanePosition(boxH: number): [number, number] {
if (!this._threeCamera) return [0, 0];
// Choose the plane position (anchor to bottom of screen, heading-invariant) ---
const bottomLeft = projectNDCToZ0(this._threeCamera, -1, -1);
const bottomRight = projectNDCToZ0(this._threeCamera, +1, -1);
let cx: number, cy: number;
if (bottomLeft && bottomRight) {
// Midpoint of the bottom-of-screen segment on Z=0:
const mx = 0.5 * (bottomLeft.x + bottomRight.x);
const my = 0.5 * (bottomLeft.y + bottomRight.y);
// Tangent along the bottom line (unit):
let dx = bottomRight.x - bottomLeft.x;
let dy = bottomRight.y - bottomLeft.y;
const len = Math.hypot(dx, dy) || 1;
dx /= len;
dy /= len;
// Inward normal n = +90° rotation of d in XY plane:
// d = (dx, dy) -> n = (-dy, dx)
let nx = -dy;
let ny = dx;
// Ensure n points "into the screen":
const midIn = projectNDCToZ0(this._threeCamera, 0, -0.5);
if (midIn) {
const vx = midIn.x - mx;
const vy = midIn.y - my;
if (vx * nx + vy * ny < 0) {
nx = -nx;
ny = -ny;
}
}
// Place the plane so its bottom edge lies on the bottom-of-screen line:
cx = mx + nx * (boxH * 0.5);
cy = my + ny * (boxH * 0.5);
} else {
// Fallback to the camera center projected on Z=0 if bottom line not visible:
const centerRay = projectNDCToZ0(this._threeCamera, 0, 0);
if (centerRay) {
cx = centerRay.x;
cy = centerRay.y;
} else {
// Fallback to the camera position if the center ray is not visible:
cx = this._threeCamera.position.x;
cy = this._threeCamera.position.y;
}
}
return [cx, cy];
}
updatePosition(): void {
// Update the 3D camera position and rotation.
if (this._threeCamera) {
const angle = -gdjs.toRad(this._layer.getCameraRotation());
this._threeCamera.position.x = this._layer.getCameraX();
this._threeCamera.position.y = -this._layer.getCameraY(); // scene is mirrored on Y
this._threeCamera.rotation.z = angle;
if (this._threeCamera instanceof THREE.OrthographicCamera) {
this._threeCamera.zoom = this._layer.getCameraZoom();
this._threeCamera.updateProjectionMatrix();
this._threeCamera.position.z = this._layer.getCameraZ(null);
} else {
this._threeCamera.position.z = this._layer.getCameraZ(
this._threeCamera.fov
);
}
}
let effectivePixiZoom = 1;
const angle = -gdjs.toRad(this._layer.getCameraRotation());
const angleCosValue = Math.cos(angle);
const angleSinValue = Math.sin(angle);
// Update the 2D plane in the 3D world position, size and rotation,
// and update the 2D Pixi container position, size and rotation.
if (this._threeCamera && this._threePlaneMesh) {
const [boxW, boxH] = this._get2DPlaneSize();
if (boxW === 0 || boxH === 0) {
// No size means the 2D plane is not visible.
this._threePlaneMesh.visible = false;
} else {
this._threePlaneMesh.visible = true;
const [cx, cy] = this._get2DPlanePosition(boxH);
// Update the 2D plane size, position and rotation (so 2D remains upright).
// Plane size (geometry is 1×1).
this._threePlaneMesh.scale.set(boxW, boxH, 1);
this._threePlaneMesh.position.set(cx, -cy, 0);
this._threePlaneMesh.rotation.set(0, 0, -angle);
// Update the 2D Pixi container size and rotation to match the "zoom" (which comes from the 2D plane size)
// rotation and position.
effectivePixiZoom = this._layer.getWidth() / boxW; // == height/boxH
this._pixiContainer.scale.set(effectivePixiZoom, effectivePixiZoom);
this._pixiContainer.rotation = angle;
const followX = cx;
const followY = -cy;
const centerX2d =
followX * effectivePixiZoom * angleCosValue -
followY * effectivePixiZoom * angleSinValue;
const centerY2d =
followX * effectivePixiZoom * angleSinValue +
followY * effectivePixiZoom * angleCosValue;
this._pixiContainer.position.x =
this._layer.getWidth() / 2 - centerX2d;
this._pixiContainer.position.y =
this._layer.getHeight() / 2 - centerY2d;
}
}
// 2D only (no 3D rendering and so no 2D plane in the 3D world):
// Update the 2D Pixi container position, size and rotation.
if (!this._threeCamera || !this._threePlaneMesh) {
effectivePixiZoom = this._layer.getCameraZoom();
this._pixiContainer.rotation = angle;
this._pixiContainer.scale.x = effectivePixiZoom;
this._pixiContainer.scale.y = effectivePixiZoom;
const centerX =
this._layer.getCameraX() * effectivePixiZoom * angleCosValue -
this._layer.getCameraY() * effectivePixiZoom * angleSinValue;
const centerY =
this._layer.getCameraX() * effectivePixiZoom * angleSinValue +
this._layer.getCameraY() * effectivePixiZoom * angleCosValue;
this._pixiContainer.position.x = this._layer.getWidth() / 2 - centerX;
this._pixiContainer.position.y = this._layer.getHeight() / 2 - centerY;
}
// Pixel rounding for the Pixi rendering (be it for 2D only
// or for the 2D rendering shown in the 2D plane in the 3D world).
if (
this._layer.getRuntimeScene().getGame().getPixelsRounding() &&
(cosValue === 0 || sinValue === 0) &&
Number.isInteger(zoomFactor)
(angleCosValue === 0 || angleSinValue === 0) &&
Number.isInteger(effectivePixiZoom)
) {
// Camera rounding is important for pixel perfect games.
// Otherwise, the camera position fractional part is added to
@@ -467,39 +919,12 @@ namespace gdjs {
);
}
}
if (this._threeCamera) {
// TODO (3D) - improvement: handle camera rounding like down for PixiJS?
this._threeCamera.position.x = this._layer.getCameraX();
this._threeCamera.position.y = -this._layer.getCameraY(); // Inverted because the scene is mirrored on Y axis.
this._threeCamera.rotation.z = angle;
if (this._threeCamera instanceof THREE.OrthographicCamera) {
this._threeCamera.zoom = this._layer.getCameraZoom();
this._threeCamera.updateProjectionMatrix();
this._threeCamera.position.z = this._layer.getCameraZ(null);
} else {
this._threeCamera.position.z = this._layer.getCameraZ(
this._threeCamera.fov
);
}
if (this._threePlaneMesh) {
// Adapt the plane size so that it covers the whole screen.
this._threePlaneMesh.scale.x = this._layer.getWidth() / zoomFactor;
this._threePlaneMesh.scale.y = this._layer.getHeight() / zoomFactor;
// Adapt the plane position so that it's always displayed on the whole screen.
this._threePlaneMesh.position.x = this._threeCamera.position.x;
this._threePlaneMesh.position.y = -this._threeCamera.position.y; // Inverted because the scene is mirrored on Y axis.
this._threePlaneMesh.rotation.z = -angle;
}
}
}
updateResolution() {
if (this._threeEffectComposer) {
const game = this._layer.getRuntimeScene().getGame();
this._threeEffectComposer.setPixelRatio(window.devicePixelRatio);
this._threeEffectComposer.setSize(
game.getGameResolutionWidth(),
game.getGameResolutionHeight()

View File

@@ -309,7 +309,7 @@ namespace gdjs {
}
unloadResource(resourceData: ResourceData): void {
const loadedFont = this._loadedFontsData.get(resourceData);
const loadedFont = this._loadedFontsData.getFromName(resourceData.name);
if (loadedFont) {
this._loadedFontsData.delete(resourceData);
}

View File

@@ -103,6 +103,10 @@ namespace gdjs {
if (!existingTexture) {
return this._invalidTexture;
}
if (existingTexture.destroyed) {
logger.error('Texture for ' + resourceName + ' is not valid anymore.');
return this._invalidTexture;
}
if (!existingTexture.valid) {
logger.error(
'Texture for ' +

View File

@@ -101,6 +101,7 @@ namespace gdjs {
this._threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
this._threeRenderer.useLegacyLights = true;
this._threeRenderer.autoClear = false;
this._threeRenderer.pixelRatio = window.devicePixelRatio;
this._threeRenderer.setSize(
this._game.getGameResolutionWidth(),
this._game.getGameResolutionHeight()
@@ -761,7 +762,7 @@ namespace gdjs {
};
// @ts-ignore
canvas.onwheel = function (event) {
manager.onMouseWheel(-event.deltaY);
manager.onMouseWheel(-event.deltaY, event.deltaX, event.deltaZ);
};
// Touches:
@@ -784,6 +785,7 @@ namespace gdjs {
touch.pageY
);
manager.onTouchMove(touch.identifier, pos[0], pos[1]);
manager.onTouchMove(touch.identifier, pos[0], pos[1]);
// This works because touch events are sent
// when they continue outside of the canvas.
if (manager.isSimulatingMouseWithTouch()) {

View File

@@ -115,9 +115,10 @@ namespace gdjs {
const runtimeLayerRenderingType = runtimeLayer.getRenderingType();
const layerHas3DObjectsToRender = runtimeLayerRenderer.has3DObjects();
if (
runtimeLayerRenderingType ===
!this._runtimeScene.getGame().isInGameEdition() &&
(runtimeLayerRenderingType ===
gdjs.RuntimeLayerRenderingType.TWO_D ||
!layerHas3DObjectsToRender
!layerHas3DObjectsToRender)
) {
// Render a layer with 2D rendering (PixiJS) only if layer is configured as is
// or if there is no 3D object to render.

View File

@@ -41,18 +41,71 @@ namespace gdjs {
return supportedCompressionMethods;
};
/**
* The desired status of the game, used for previews or in-game edition.
* Either stored in the options generated by the preview or in the URL
* in case of a hard reload.
*/
export type RuntimeGameStatus = {
isPaused: boolean;
isInGameEdition: boolean;
sceneName: string | null;
injectedExternalLayoutName: string | null;
skipCreatingInstancesFromScene: boolean;
eventsBasedObjectType: string | null;
eventsBasedObjectVariantName: string | null;
editorId: string | null;
editorCamera3D?: EditorCameraState;
};
/**
* Read the desired status of the game from the URL. Only useful for previews
* when hard reloaded.
*/
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
try {
const url = new URL(location.href);
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
if (!runtimeGameStatus) return null;
const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
return {
isPaused: !!parsedRuntimeGameStatus.isPaused,
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
sceneName: '' + parsedRuntimeGameStatus.sceneName,
injectedExternalLayoutName:
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
skipCreatingInstancesFromScene:
!!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
eventsBasedObjectType: parsedRuntimeGameStatus.eventsBasedObjectType,
eventsBasedObjectVariantName:
parsedRuntimeGameStatus.eventsBasedObjectVariantName,
editorId: parsedRuntimeGameStatus.editorId,
editorCamera3D: parsedRuntimeGameStatus.editorCamera3D,
};
} catch (e) {
return null;
}
};
/** Options given to the game at startup. */
export type RuntimeGameOptions = {
/** if true, force fullscreen. */
forceFullscreen?: boolean;
/** if true, game is run as a preview launched from an editor. */
isPreview?: boolean;
/** The name of the external layout to create in the scene at position 0;0. */
injectExternalLayout?: string;
/** if set, the status of the game to be restored. */
initialRuntimeGameStatus?: RuntimeGameStatus;
/** Script files, used for hot-reloading. */
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without events. */
projectDataOnlyExport?: boolean;
/** if true, export is a partial preview without reloading libraries. */
shouldReloadLibraries?: boolean;
/** if true, export is a partial preview without generating events. */
shouldGenerateScenesEventsCode?: boolean;
/** if true, preview is launched from GDevelop native mobile app. */
nativeMobileApp?: boolean;
/** The address of the debugger server, to reach out using WebSocket. */
@@ -139,7 +192,13 @@ namespace gdjs {
_gameResolutionHeight: integer;
_originalWidth: float;
_originalHeight: float;
_resizeMode: 'adaptWidth' | 'adaptHeight' | string;
_resizeMode:
| ''
| 'scaleOuter'
| 'adaptWidth'
| 'adaptHeight'
| 'native'
| string;
_adaptGameResolutionAtRuntime: boolean;
_scaleMode: 'linear' | 'nearest';
_pixelsRounding: boolean;
@@ -171,12 +230,8 @@ namespace gdjs {
_hasJustResumed: boolean = false;
//Inputs :
_inputManager: InputManager;
private _inputManager: InputManager;
/**
* Allow to specify an external layout to insert in the first scene.
*/
_injectExternalLayout: any;
_options: RuntimeGameOptions;
/**
@@ -194,6 +249,7 @@ namespace gdjs {
_sessionMetricsInitialized: boolean = false;
_disableMetrics: boolean = false;
_isPreview: boolean;
_isInGameEdition: boolean;
/**
* The capture manager, used to manage captures (screenshots, videos, etc...).
@@ -203,12 +259,27 @@ namespace gdjs {
/** True if the RuntimeGame has been disposed and should not be used anymore. */
_wasDisposed: boolean = false;
_inGameEditor: InGameEditor | null;
/**
* @param data The object (usually stored in data.json) containing the full project data
* @param options The game options
*/
constructor(data: ProjectData, options?: RuntimeGameOptions) {
this._options = options || {};
this._isPreview = this._options.isPreview || false;
if (this._isPreview) {
// Check if we need to restore the state from the URL, which is used
// when a preview is hard reloaded (search for `hardReload`).
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
if (runtimeGameStatusFromUrl) {
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
}
}
this._isInGameEdition =
this._options.initialRuntimeGameStatus?.isInGameEdition || false;
this._variables = new gdjs.VariablesContainer(data.variables);
this._variablesByExtensionName = new Map<
string,
@@ -237,7 +308,12 @@ namespace gdjs {
getGlobalResourceNames(data),
data.layouts
);
this._inGameEditor = this._isInGameEdition
? new gdjs.InGameEditor(this, data)
: null;
this._debuggerClient = gdjs.DebuggerClient
? new gdjs.DebuggerClient(this)
: null;
this._effectsManager = new gdjs.EffectsManager();
this._maxFPS = this._data.properties.maxFPS;
this._minFPS = this._data.properties.minFPS;
@@ -265,17 +341,12 @@ namespace gdjs {
);
this._sceneStack = new gdjs.SceneStack(this);
this._inputManager = new gdjs.InputManager();
this._injectExternalLayout = this._options.injectExternalLayout || '';
this._debuggerClient = gdjs.DebuggerClient
? new gdjs.DebuggerClient(this)
: null;
this._captureManager = gdjs.CaptureManager
? new gdjs.CaptureManager(
this._renderer,
this._options.captureOptions || {}
)
: null;
this._isPreview = this._options.isPreview || false;
this._sessionId = null;
this._playerId = null;
@@ -311,6 +382,9 @@ namespace gdjs {
* @param projectData The object (usually stored in data.json) containing the full project data
*/
setProjectData(projectData: ProjectData): void {
if (this._inGameEditor) {
this._inGameEditor.onProjectDataChange(projectData);
}
this._data = projectData;
this._updateSceneAndExtensionsData();
this._resourcesLoader.setResources(
@@ -485,6 +559,55 @@ namespace gdjs {
return eventsBasedObjectData;
}
getEventsBasedObjectVariantData(
type: string,
variantName: string
): EventsBasedObjectVariantData | null {
const eventsBasedObjectData = this.getEventsBasedObjectData(type);
if (!eventsBasedObjectData) {
return null;
}
return gdjs.RuntimeGame._getEventsBasedObjectVariantData(
eventsBasedObjectData,
variantName
);
}
static _getEventsBasedObjectVariantData(
eventsBasedObjectData: EventsBasedObjectData,
variantName: string
): EventsBasedObjectVariantData {
if (!eventsBasedObjectData.defaultVariant) {
eventsBasedObjectData.defaultVariant = {
...eventsBasedObjectData,
name: '',
};
}
// Legacy events-based objects don't have any instance in their default
// variant since there wasn't a graphical editor at the time.
// In this case, the editor doesn't allow to choose a variant, but a
// variant may have stayed after a user rolled back the extension.
// This variant must be ignored to match what the editor shows.
const isForcedToOverrideEventsBasedObjectChildrenConfiguration =
eventsBasedObjectData.defaultVariant.instances.length == 0;
if (isForcedToOverrideEventsBasedObjectChildrenConfiguration) {
return eventsBasedObjectData.defaultVariant;
}
let usedVariantData: EventsBasedObjectVariantData =
eventsBasedObjectData.defaultVariant;
for (
let variantIndex = 0;
variantIndex < eventsBasedObjectData.variants.length;
variantIndex++
) {
const variantData = eventsBasedObjectData.variants[variantIndex];
if (variantData.name === variantName) {
usedVariantData = variantData;
}
}
return usedVariantData;
}
/**
* Get the data associated to a scene.
*
@@ -523,6 +646,22 @@ namespace gdjs {
return false;
}
/**
* Get the data associated to a scene.
*
* @param name The name of the scene.
* @return The data associated to the scene or null if not found.
*/
getSceneData(sceneName: string): LayoutData | null {
for (let i = 0, len = this._data.layouts.length; i < len; ++i) {
const sceneData = this._data.layouts[i];
if (sceneData.name == sceneName) {
return sceneData;
}
}
return null;
}
/**
* Get the data associated to an external layout.
*
@@ -594,7 +733,7 @@ namespace gdjs {
this._gameResolutionWidth = width;
this._gameResolutionHeight = height;
if (this._adaptGameResolutionAtRuntime) {
if (this._adaptGameResolutionAtRuntime || this._isInGameEdition) {
if (
gdjs.RuntimeGameRenderer &&
gdjs.RuntimeGameRenderer.getWindowInnerWidth &&
@@ -606,7 +745,10 @@ namespace gdjs {
gdjs.RuntimeGameRenderer.getWindowInnerHeight();
// Enlarge either the width or the eight to fill the inner window space.
if (this._resizeMode === 'adaptWidth') {
if (this._isInGameEdition) {
this._gameResolutionWidth = windowInnerWidth;
this._gameResolutionHeight = windowInnerHeight;
} else if (this._resizeMode === 'adaptWidth') {
this._gameResolutionWidth =
(this._gameResolutionHeight * windowInnerWidth) /
windowInnerHeight;
@@ -735,9 +877,9 @@ namespace gdjs {
if (this._paused === enable) return;
this._paused = enable;
if (this._inGameEditor) this._inGameEditor.activate(enable);
if (this._debuggerClient) {
if (this._paused) this._debuggerClient.sendGamePaused();
else this._debuggerClient.sendGameResumed();
this._debuggerClient.sendRuntimeGameStatus();
}
}
@@ -914,11 +1056,16 @@ namespace gdjs {
await loadAssets(onProgress);
await loadingScreen.unload();
this.pause(false);
if (!this._isInGameEdition) {
this.pause(false);
}
}
private _getFirstSceneName(): string {
const firstSceneName = this._data.firstLayout;
const firstSceneName =
this._options.initialRuntimeGameStatus?.sceneName ||
this._data.firstLayout;
return this.hasScene(firstSceneName)
? firstSceneName
: // There is always at least a scene
@@ -938,10 +1085,41 @@ namespace gdjs {
this._forceGameResolutionUpdate();
// Load the first scene
this._sceneStack.push(
this._getFirstSceneName(),
this._injectExternalLayout
);
const sceneName = this._getFirstSceneName();
const externalLayoutName =
this._options.initialRuntimeGameStatus?.injectedExternalLayoutName ||
null;
if (this._inGameEditor) {
const eventsBasedObjectType =
this._options.initialRuntimeGameStatus?.eventsBasedObjectType ||
null;
const eventsBasedObjectVariantName =
this._options.initialRuntimeGameStatus
?.eventsBasedObjectVariantName || null;
const editorId =
this._options.initialRuntimeGameStatus?.editorId || null;
const editorCamera3D =
this._options.initialRuntimeGameStatus?.editorCamera3D || null;
this._inGameEditor.switchToSceneOrVariant(
editorId,
sceneName,
externalLayoutName,
eventsBasedObjectType,
eventsBasedObjectVariantName,
editorCamera3D
);
} else {
if (sceneName) {
this.getSceneStack().replace({
sceneName,
externalLayoutName:
externalLayoutName === null ? undefined : externalLayoutName,
skipCreatingInstancesFromScene: !!externalLayoutName,
clear: true,
});
}
}
this._watermark.displayAtStartup();
//Uncomment to profile the first x frames of the game.
@@ -967,15 +1145,33 @@ namespace gdjs {
}
// The standard game loop
let lastFrameSceneName: string | null = null;
let accumulatedElapsedTime = 0;
this._hasJustResumed = false;
this._renderer.startGameLoop((lastCallElapsedTime) => {
try {
if (this._paused) {
return true;
// Watch the scene name to automatically update debugger when a scene is changed.
if (this._debuggerClient) {
const currentScene = (
this._inGameEditor || this.getSceneStack()
).getCurrentScene();
if (
currentScene &&
currentScene.getName() !== lastFrameSceneName
) {
lastFrameSceneName = currentScene.getName();
this._debuggerClient.sendRuntimeGameStatus();
}
}
// Skip the frame if we rendering frames too fast
// If the game is edited, update the target framerate according to interactions.
// Do it now (before frame skip), so that if a user interaction happens
// we don't wait for a frame to pass at the current, probably very slow framerate.
if (this._paused && this._inGameEditor) {
this._inGameEditor.updateTargetFramerate(lastCallElapsedTime);
}
// Skip the frame if we rendering frames too fast.
accumulatedElapsedTime += lastCallElapsedTime;
if (
this._maxFPS > 0 &&
@@ -992,17 +1188,36 @@ namespace gdjs {
// Manage resize events.
if (this._notifyScenesForGameResolutionResize) {
this._sceneStack.onGameResolutionResized();
if (this._inGameEditor) {
this._inGameEditor.onGameResolutionResized();
} else {
this._sceneStack.onGameResolutionResized();
}
this._notifyScenesForGameResolutionResize = false;
}
// Render and step the scene.
if (this._sceneStack.step(elapsedTime)) {
this.getInputManager().onFrameEnded();
// Render and possibly step the game.
if (this._paused) {
if (this._inGameEditor) {
// The game is paused for edition: the in-game editor runs and render
// the scene.
this._inGameEditor.updateAndRender();
} else {
// The game is paused (for debugging): the rendering of the scene is done,
// but the game logic is not executed (no full "step").
this._sceneStack.renderWithoutStep();
}
} else {
// The game is not paused (and so, not edited): both the rendering
// and game logic (a full "step") is executed.
if (!this._sceneStack.step(elapsedTime)) {
return false; // Return if game asked to be stopped.
}
this._hasJustResumed = false;
return true;
}
return false;
this.getInputManager().onFrameEnded();
return true;
} catch (e) {
if (this._debuggerClient)
this._debuggerClient.onUncaughtException(e);
@@ -1323,6 +1538,37 @@ namespace gdjs {
return this._isPreview;
}
/**
* Check if the game loop is paused, for debugging/edition purposes.
* @returns true if the current game is paused
*/
isPaused(): boolean {
return this._paused;
}
/**
* Check if the game should display in-game edition tools or not.
* @returns true if the current game is being edited.
*/
isInGameEdition(): boolean {
return this._isInGameEdition;
}
/**
* Return in-game editor.
*/
getInGameEditor(): InGameEditor | null {
return this._inGameEditor;
}
/**
* Set the maximum FPS of the game.
* @param maximumFps The maximum FPS.
*/
setMaximumFps(maximumFps: integer) {
this._maxFPS = maximumFps;
}
/**
* Check if the game should call GDevelop development APIs or not.
*
@@ -1384,29 +1630,24 @@ namespace gdjs {
syncOptions: GetNetworkSyncDataOptions
): GameNetworkSyncData | null {
const syncData: GameNetworkSyncData = {
var:
syncOptions.syncGameVariables === false
? undefined
: this._variables.getNetworkSyncData(syncOptions),
var: this._variables.getNetworkSyncData(syncOptions),
sm: syncOptions.syncSounds
? this.getSoundManager().getNetworkSyncData()
: undefined,
ss: this._sceneStack.getNetworkSyncData(syncOptions) || undefined,
};
if (syncOptions.syncGameVariables !== false) {
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;
}
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) &&

View File

@@ -1437,6 +1437,22 @@ namespace gdjs {
return this.hidden;
}
/**
* Return the width of the object before any custom size is applied.
* @return The width of the object
*/
getOriginalWidth(): float {
return this.getWidth();
}
/**
* Return the width of the object before any custom size is applied.
* @return The width of the object
*/
getOriginalHeight(): float {
return this.getHeight();
}
/**
* Set the width of the object, if applicable.
* @param width The new width in pixels.

View File

@@ -128,13 +128,14 @@ namespace gdjs {
/**
* Load the runtime scene from the given scene.
* @param sceneAndExtensionsData An object containing the scene data.
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
* @param options Options to change what is loaded.
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
*/
loadFromScene(
sceneAndExtensionsData: SceneAndExtensionsData | null,
options?: {
excludedObjectNames?: Set<string>;
skipCreatingInstances?: boolean;
skipStoppingSoundsOnStartup?: boolean;
}
) {
@@ -195,18 +196,17 @@ namespace gdjs {
this.registerObject(sceneData.objects[i]);
}
// Create initial instances of objects.
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true,
{
excludedObjectNames: options?.excludedObjectNames,
}
);
//Create initial instances of objects
if (!options || !options.skipCreatingInstances) {
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true
);
}
// Set up the default z order (for objects created from events)
this._setLayerDefaultZOrders();
@@ -379,7 +379,7 @@ namespace gdjs {
}
/**
* Step and render the scene.
* Step (execute the game logic) and render the scene.
* @param elapsedTime In milliseconds
* @return true if the game loop should continue, false if a scene change/push/pop
* or a game stop was requested.
@@ -439,6 +439,21 @@ namespace gdjs {
if (this._profiler) {
this._profiler.end('callbacks and extensions (post-events)');
}
this.render();
this._isJustResumed = false;
if (this._profiler) {
this._profiler.end('render');
}
if (this._profiler) {
this._profiler.endFrame();
}
return !!this.getRequestedChange();
}
/**
* Render the scene (but do not execute the game logic).
*/
render() {
if (this._profiler) {
this._profiler.begin('objects (pre-render, effects update)');
}
@@ -468,21 +483,6 @@ namespace gdjs {
);
}
this._isJustResumed = false;
this.render();
if (this._profiler) {
this._profiler.end('render');
}
if (this._profiler) {
this._profiler.endFrame();
}
return !!this.getRequestedChange();
}
/**
* Render the PIXI container associated to the runtimeScene.
*/
render() {
this._renderer.render();
}

View File

@@ -1,15 +1,11 @@
namespace gdjs {
const logger = new gdjs.Logger('Scene stack');
const debugLogger = new gdjs.Logger('Scene stack - Debug');
// Comment this to see message logs and ease debugging:
gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(
'Scene stack - Debug'
);
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
interface PushSceneOptions {
sceneName: string;
externalLayoutName?: string;
getExcludedObjectNames?: (runtimeScene: RuntimeScene) => Set<string>;
skipCreatingInstancesFromScene?: boolean;
skipStoppingSoundsOnStartup?: boolean;
}
@@ -97,10 +93,10 @@ namespace gdjs {
renderWithoutStep(): boolean {
this._throwIfDisposed();
if (this._stack.length === 0) {
const currentScene = this.getCurrentScene();
if (!currentScene) {
return false;
}
const currentScene = this._stack[this._stack.length - 1];
currentScene.render();
return true;
}
@@ -150,10 +146,10 @@ namespace gdjs {
const sceneName =
typeof options === 'string' ? options : options.sceneName;
const getExcludedObjectNames =
const skipCreatingInstancesFromScene =
typeof options === 'string'
? undefined
: options.getExcludedObjectNames;
? false
: options.skipCreatingInstancesFromScene;
const skipStoppingSoundsOnStartup =
typeof options === 'string'
? false
@@ -174,7 +170,7 @@ namespace gdjs {
return this._loadNewScene({
sceneName,
externalLayoutName,
getExcludedObjectNames,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
}
@@ -184,7 +180,7 @@ namespace gdjs {
this._loadNewScene({
sceneName,
externalLayoutName,
getExcludedObjectNames,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
this._isNextLayoutLoading = false;
@@ -201,9 +197,7 @@ namespace gdjs {
newScene.loadFromScene(
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
{
excludedObjectNames: options.getExcludedObjectNames
? options.getExcludedObjectNames(newScene)
: undefined,
skipCreatingInstances: options.skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup: options.skipStoppingSoundsOnStartup,
}
);
@@ -339,7 +333,8 @@ namespace gdjs {
this._sceneStackSyncDataToApply = null;
const getExcludedObjectNames = options && options.getExcludedObjectNames;
const skipCreatingInstancesFromScene =
!!options && !!options.preventInitialInstancesCreation;
const skipStoppingSoundsOnStartup =
!!options && !!options.preventSoundsStoppingOnStartup;
@@ -354,7 +349,7 @@ namespace gdjs {
const sceneSyncData = sceneStackSyncData[i];
const newScene = this.push({
sceneName: sceneSyncData.name,
getExcludedObjectNames,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
if (newScene) {
@@ -364,7 +359,6 @@ namespace gdjs {
hasMadeChangeToStack = true;
return hasMadeChangeToStack;
}
// If this method is called, we are a client.
// We trust the host to be the source of truth for the scene stack.
// So we loop through the scenes in the stack given by the host and either:
@@ -380,10 +374,9 @@ namespace gdjs {
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
);
// We have fewer scenes in the stack than the host, let's add the scene.
// By definition, there is nothing to clear because we're already at the top of the stack.
const newScene = this.push({
sceneName: sceneSyncData.name,
getExcludedObjectNames,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
@@ -397,23 +390,16 @@ namespace gdjs {
debugLogger.info(
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} is not the same as the expected ${
sceneSyncData.name
}, replacing it.`
}, replacing.`
);
// The scene does not correspond to the scene at this position in our stack
// Let's unload everything after this position to reconstruct the stack.
if (this._stack.length > i + 1) {
debugLogger.info(
`Unloading ${this._stack.length - (i + 1)} scenes after position ${i}.`
);
this.pop(this._stack.length - (i + 1));
}
// Let's unload everything after this position to recreate the stack.
const newScene = this.replace({
sceneName: sceneSyncData.name,
clear: false,
getExcludedObjectNames,
clear: true,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
@@ -447,26 +433,16 @@ namespace gdjs {
debugLogger.info(
`Scene at position ${i} and name ${sceneAtThisPositionInOurStack.getName()} has a different networkId ${
sceneAtThisPositionInOurStack.networkId
} than the expected ${sceneSyncData.networkId}, replacing it.`
} than the expected ${sceneSyncData.networkId}, replacing.`
);
// The scene is in the stack but has a different networkId
// This can happen if the host has restarted the scene
// We can't just update the networkId of the scene in the stack
// We need to replace it with a new scene.
// Like for the case where it's a totally different scene,
// we need to unload everything after this position to reconstruct the stack.
if (this._stack.length > i + 1) {
debugLogger.info(
`Unloading ${this._stack.length - (i + 1)} scenes after position ${i}.`
);
this.pop(this._stack.length - (i + 1));
}
// We need to replace it with a new scene
const newScene = this.replace({
sceneName: sceneSyncData.name,
clear: false,
getExcludedObjectNames,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;

View File

@@ -190,15 +190,13 @@ namespace gdjs {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
if (initialInstanceData.flippedX) {
this.flipX(initialInstanceData.flippedX);
}
if (initialInstanceData.flippedY) {
this.flipY(initialInstanceData.flippedY);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
this.flipX(!!initialInstanceData.flippedX);
this.flipY(!!initialInstanceData.flippedY);
}
/**
@@ -853,6 +851,14 @@ namespace gdjs {
this.setHeight(newHeight);
}
override getOriginalWidth(): float {
return this._renderer.getUnscaledWidth() * this._preScale;
}
override getOriginalHeight(): float {
return this._renderer.getUnscaledHeight() * this._preScale;
}
/**
* Change the scale on X and Y axis of the object.
*

View File

@@ -2,7 +2,10 @@ import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { SelectionBox } from 'three/examples/jsm/interactive/SelectionBox';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { Pass } from 'three/examples/jsm/postprocessing/Pass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
@@ -21,7 +24,10 @@ declare global {
GLTF,
DRACOLoader,
SkeletonUtils,
TransformControls,
SelectionBox,
EffectComposer,
OutlinePass,
Pass,
RenderPass,
ShaderPass,

View File

@@ -76,32 +76,3 @@ declare namespace gdjs {
/** The global cc object from Cocos2D-Js. */
declare var cc: any;
/**
* Fixes https://github.com/microsoft/TypeScript/issues/16655 for `Array.prototype.filter()`
* For example, using the fix the type of `bar` is `string[]` in the below snippet as it should be.
*
* const foo: (string | null | undefined)[] = [];
* const bar = foo.filter(Boolean);
*
* For related definitions, see https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts
*
* Original licenses apply, see
* - https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt
* - https://stackoverflow.com/help/licensing
*/
/** See https://stackoverflow.com/a/51390763/1470607 */
type Falsy = false | 0 | '' | null | undefined;
interface Array<T> {
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(
predicate: BooleanConstructor,
thisArg?: any
): Exclude<S, Falsy>[];
}

View File

@@ -18,6 +18,7 @@ declare interface ProjectData {
layouts: LayoutData[];
externalLayouts: ExternalLayoutData[];
eventsFunctionsExtensions: EventsFunctionsExtensionData[];
areEffectsHiddenInEditor?: boolean;
}
declare interface EventsFunctionsVariablesData {
@@ -43,9 +44,8 @@ declare type GetNetworkSyncDataOptions = {
playerNumber?: number;
isHost?: boolean;
syncObjectIdentifiers?: boolean;
shouldExcludeVariableFromData?: (variable: Variable) => boolean;
syncAllVariables?: boolean;
syncAllBehaviors?: boolean;
syncGameVariables?: boolean;
syncSceneTimers?: boolean;
syncOnceTriggers?: boolean;
syncSounds?: boolean;
@@ -58,12 +58,11 @@ declare type GetNetworkSyncDataOptions = {
declare type UpdateFromNetworkSyncDataOptions = {
clearSceneStack?: boolean;
getExcludedObjectNames?: (runtimeScene: RuntimeScene) => Set<string>;
preventInitialInstancesCreation?: boolean;
preventSoundsStoppingOnStartup?: boolean;
clearInputs?: boolean;
keepControl?: boolean;
ignoreVariableOwnership?: boolean;
shouldExcludeVariableFromUpdate?: (variable: Variable) => boolean;
};
/** Object containing basic properties for all objects synchronizing over the network. */
@@ -290,6 +289,21 @@ declare interface LayoutData extends InstanceContainerData {
usedResources: ResourceReference[];
resourcesPreloading?: 'at-startup' | 'never' | 'inherit';
resourcesUnloading?: 'at-scene-exit' | 'never' | 'inherit';
uiSettings: InstancesEditorSettings;
}
declare interface InstancesEditorSettings {
grid: boolean;
gridType: 'rectangular' | 'isometric';
gridWidth: float;
gridHeight: float;
gridDepth?: float;
gridOffsetX: float;
gridOffsetY: float;
gridOffsetZ?: float;
gridColor: int;
gridAlpha: float;
snap: boolean;
}
declare interface LayoutNetworkSyncData {
@@ -385,6 +399,8 @@ declare interface EventsBasedObjectVariantData extends InstanceContainerData {
instances: InstanceData[];
objects: ObjectData[];
layers: LayerData[];
usedResources: ResourceReference[];
editionSettings: InstancesEditorSettings;
}
declare interface BehaviorSharedData {
@@ -396,13 +412,17 @@ declare interface ExternalLayoutData {
name: string;
associatedLayout: string;
instances: InstanceData[];
editionSettings: InstancesEditorSettings;
}
declare interface InstanceData {
declare interface InstancePersistentUuidData {
persistentUuid: string;
}
declare interface InstanceData extends InstancePersistentUuidData {
layer: string;
locked: boolean;
locked?: boolean;
sealed?: boolean;
name: string;
x: number;
@@ -453,8 +473,10 @@ declare interface LayerData {
camera3DFieldOfView?: float;
camera3DFarPlaneDistance?: float;
camera3DNearPlaneDistance?: float;
camera2DPlaneMaxDrawingDistance?: float;
isLightingLayer: boolean;
followBaseLayerCamera: boolean;
isLocked?: boolean;
}
declare interface CameraData {
@@ -499,7 +521,7 @@ declare interface ProjectPropertiesData {
pixelsRounding: boolean;
antialiasingMode: 'none' | 'MSAA';
antialisingEnabledOnMobile: boolean;
sizeOnStartupMode: string;
sizeOnStartupMode: '' | 'scaleOuter' | 'adaptWidth' | 'adaptHeight';
version: string;
name: string;
author: string;

View File

@@ -4,6 +4,6 @@ declare type SceneSaveState = {
};
declare type GameSaveState = {
gameNetworkSyncData: GameNetworkSyncData | null;
gameNetworkSyncData: GameNetworkSyncData;
layoutNetworkSyncDatas: SceneSaveState[];
};

View File

@@ -126,38 +126,26 @@ namespace gdjs {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): UnnamedVariableNetworkSyncData | undefined {
const syncedPlayerNumber = syncOptions.playerNumber;
const isHost = syncOptions.isHost;
const variableOwner = this.getPlayerOwnership();
if (
// Variable undefined.
this.isUndefinedInContainer()
)
return;
const variableOwner = this.getPlayerOwnership();
if (syncOptions.shouldExcludeVariableFromData) {
// Saving for "save state": serialize all variables unless excluded.
if (syncOptions.shouldExcludeVariableFromData(this)) {
return;
}
} else {
// Saving for "multiplayer": only serialize the variable if owned by the player.
const syncedPlayerNumber = syncOptions.playerNumber;
const isHost = syncOptions.isHost;
if (
this.isUndefinedInContainer() ||
// If we force sync everything, we don't look at the ownership.
(!syncOptions.syncAllVariables &&
// Variable marked as not to be synchronized.
variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Variable is owned by host but this player number is not the host.
variableOwner === 0 &&
!isHost) ||
// Variable is owned by a player but not getting sync data for this player number.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
) {
// In those cases, the variable should not be synchronized.
return;
}
(variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Variable is owned by host but this player number is not the host.
variableOwner === 0 &&
!isHost) ||
// Variable is owned by a player but not getting sync data for this player number.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)))
) {
// In those cases, the variable should not be synchronized.
return;
}
const variableType = this.getType();
@@ -264,13 +252,6 @@ namespace gdjs {
const variableData =
gdjs.Variable.getVariableDataFromNetworkSyncData(networkSyncData);
if (
options.shouldExcludeVariableFromUpdate &&
options.shouldExcludeVariableFromUpdate(this)
) {
return;
}
if (!options.ignoreVariableOwnership) {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();

View File

@@ -19,6 +19,7 @@ const allowedExtensions = [
'.map',
'.wasm',
'.txt',
'.png',
];
// These extensions will be built with esbuild (the other will be copied).
@@ -42,6 +43,7 @@ const untransformedPaths = [
'GDJS/Runtime/FacebookInstantGames',
'GDJS/Runtime/libs/CocoonJS',
'GDJS/Runtime/libs/rbush.js',
'GDJS/Runtime/InGameEditor/Resources/primitivedrawingicon.png',
// Extensions pre-built files:
'Extensions/Leaderboards/sha256.js',

View File

@@ -1,7 +1,7 @@
{
"firstLayout": "",
"gdVersion": {
"build": 242,
"build": 241,
"major": 5,
"minor": 5,
"revision": 0
@@ -362,8 +362,7 @@
"gridAlpha": 0.8,
"snap": false,
"zoomFactor": 0.4633111824514028,
"windowMask": false,
"selectedLayer": ""
"windowMask": false
},
"objectsGroups": [],
"variables": [
@@ -404,11 +403,6 @@
"name": "Variable",
"type": "number",
"value": 0
},
{
"name": "VariableExcludedFromSave",
"type": "number",
"value": 10
}
],
"instances": [
@@ -454,8 +448,8 @@
"name": "Joystick",
"persistentUuid": "855d809a-f954-4072-86d5-1d6f0b24a066",
"width": 0,
"x": 131,
"y": 604,
"x": 176,
"y": 576,
"zOrder": 2,
"numberProperties": [],
"stringProperties": [],
@@ -844,118 +838,6 @@
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "Background",
"name": "NewText7",
"persistentUuid": "02ff696e-6d03-466c-9e4a-f993c4c45958",
"width": 0,
"x": 159,
"y": 35,
"zOrder": 39,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "749601b7-c699-47a6-81b8-d19b7cd5bca9",
"width": 0,
"x": 704,
"y": 403,
"zOrder": 40,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "502f29fe-fca1-4625-b194-49a701bda9cd",
"width": 0,
"x": 723,
"y": 323,
"zOrder": 41,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "65d4f46c-339a-4a2b-9870-799566c2456b",
"width": 0,
"x": 846,
"y": 205,
"zOrder": 42,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "3e848abb-3ab3-48ce-9176-23cd9be84fde",
"width": 0,
"x": 1094,
"y": 185,
"zOrder": 43,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "47351cf9-0485-4388-958c-94024dd13e9f",
"width": 0,
"x": 1219,
"y": 263,
"zOrder": 44,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
},
{
"angle": 0,
"customSize": false,
"height": 0,
"keepRatio": true,
"layer": "",
"name": "Coins",
"persistentUuid": "8b305063-3f81-4ef4-accc-1f36ad1bed2d",
"width": 0,
"x": 1228,
"y": 362,
"zOrder": 45,
"numberProperties": [],
"stringProperties": [],
"initialVariables": []
}
],
"objects": [
@@ -1119,14 +1001,7 @@
"updateIfNotVisible": false,
"variables": [],
"effects": [],
"behaviors": [
{
"name": "SaveConfiguration",
"type": "SaveState::SaveConfiguration",
"defaultProfilePersistence": "Persisted",
"persistedInProfiles": "coins"
}
],
"behaviors": [],
"animations": [
{
"name": "",
@@ -1284,14 +1159,7 @@
"variant": "",
"variables": [],
"effects": [],
"behaviors": [
{
"name": "SaveConfiguration",
"type": "SaveState::SaveConfiguration",
"defaultProfilePersistence": "DoNotSave",
"persistedInProfiles": ""
}
],
"behaviors": [],
"content": {},
"childrenContent": {
"Border": {
@@ -2145,49 +2013,6 @@
"lineHeight": 0,
"color": "0;0;0"
}
},
{
"assetStoreId": "",
"bold": false,
"italic": false,
"name": "NewText7",
"smoothed": true,
"type": "TextObject::Text",
"underlined": false,
"variables": [],
"effects": [],
"behaviors": [],
"string": "Excluded Variable",
"font": "",
"textAlignment": "left",
"characterSize": 20,
"color": {
"b": 0,
"g": 0,
"r": 0
},
"content": {
"bold": false,
"isOutlineEnabled": false,
"isShadowEnabled": false,
"italic": false,
"outlineColor": "255;255;255",
"outlineThickness": 2,
"shadowAngle": 90,
"shadowBlurRadius": 2,
"shadowColor": "0;0;0",
"shadowDistance": 4,
"shadowOpacity": 127,
"smoothed": true,
"underlined": false,
"text": "Excluded Variable",
"font": "",
"textAlignment": "left",
"verticalTextAlignment": "top",
"characterSize": 20,
"lineHeight": 0,
"color": "0;0;0"
}
}
],
"objectsFolderStructure": {
@@ -2220,9 +2045,6 @@
{
"objectName": "NewText6"
},
{
"objectName": "NewText7"
},
{
"objectName": "FlagBlue"
},
@@ -2295,23 +2117,6 @@
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"value": "SaveState::SetVariableSaveConfiguration"
},
"parameters": [
"",
"VariableExcludedFromSave",
"no",
""
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
@@ -2348,12 +2153,11 @@
},
{
"type": {
"value": "SaveState::CreateGameSaveStateInStorage"
"value": "SaveState::SaveGameSnapshotToStorage"
},
"parameters": [
"",
"\"test\"",
""
"\"test\""
]
}
]
@@ -2383,143 +2187,10 @@
"actions": [
{
"type": {
"value": "SaveState::RestoreGameSaveStateFromStorage"
"value": "SaveState::LoadGameSnapshotFromStorage"
},
"parameters": [
"",
"\"test\"",
"",
"no"
]
}
]
},
{
"type": "BuiltinCommonInstructions::Comment",
"color": {
"b": 109,
"g": 230,
"r": 255,
"textB": 0,
"textG": 0,
"textR": 0
},
"comment": "Press s to save and l to load manually. Check how the \"VariableExcludedFromSave\" won't be impacted."
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "KeyFromTextJustPressed"
},
"parameters": [
"",
"\"s\""
]
}
],
"actions": [
{
"type": {
"value": "SaveState::CreateGameSaveStateInStorage"
},
"parameters": [
"",
"\"manual-save\"",
""
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "KeyFromTextJustPressed"
},
"parameters": [
"",
"\"l\""
]
}
],
"actions": [
{
"type": {
"value": "SaveState::RestoreGameSaveStateFromStorage"
},
"parameters": [
"",
"\"manual-save\"",
"",
""
]
}
]
},
{
"type": "BuiltinCommonInstructions::Comment",
"color": {
"b": 109,
"g": 230,
"r": 255,
"textB": 0,
"textG": 0,
"textR": 0
},
"comment": "Press c to just save the coins and press x to load only the coins. Check how the rest won't be impacted."
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "KeyFromTextJustPressed"
},
"parameters": [
"",
"\"c\""
]
}
],
"actions": [
{
"type": {
"value": "SaveState::CreateGameSaveStateInStorage"
},
"parameters": [
"",
"\"coins-save\"",
"\"coins\""
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "KeyFromTextJustPressed"
},
"parameters": [
"",
"\"x\""
]
}
],
"actions": [
{
"type": {
"value": "SaveState::RestoreGameSaveStateFromStorage"
},
"parameters": [
"",
"\"coins-save\"",
"\"coins\"",
""
"\"test\""
]
}
]
@@ -2561,7 +2232,6 @@
},
"parameters": [
">=",
">",
"0"
]
}
@@ -2588,7 +2258,6 @@
"value": "SaveState::TimeSinceLastSave"
},
"parameters": [
"<",
"<",
"0"
]
@@ -2617,7 +2286,6 @@
},
"parameters": [
">=",
">",
"0"
]
}
@@ -2644,7 +2312,6 @@
"value": "SaveState::TimeSinceLastLoad"
},
"parameters": [
"<",
"<",
"0"
]
@@ -3041,84 +2708,6 @@
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [],
"actions": [
{
"type": {
"value": "TextContainerCapability::TextContainerBehavior::SetValue"
},
"parameters": [
"NewText7",
"Text",
"=",
"\"Excluded variable: \" + VariableExcludedFromSave"
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "DepartScene"
},
"parameters": [
""
]
}
],
"actions": [
{
"type": {
"value": "ResetTimer"
},
"parameters": [
"",
"\"ExcludedVariableIncreaseTimer\""
]
}
]
},
{
"type": "BuiltinCommonInstructions::Standard",
"conditions": [
{
"type": {
"value": "CompareTimer"
},
"parameters": [
"",
"\"ExcludedVariableIncreaseTimer\"",
">=",
"0.5"
]
}
],
"actions": [
{
"type": {
"value": "ResetTimer"
},
"parameters": [
"",
"\"ExcludedVariableIncreaseTimer\""
]
},
{
"type": {
"value": "SetNumberVariable"
},
"parameters": [
"VariableExcludedFromSave",
"+",
"1"
]
}
]
}
],
"layers": [
@@ -3230,10 +2819,6 @@
"name": "Resizable",
"type": "ResizableCapability::ResizableBehavior"
},
{
"name": "SaveConfiguration",
"type": "SaveState::SaveConfiguration"
},
{
"name": "Scale",
"type": "ScalableCapability::ScalableBehavior"
@@ -13765,7 +13350,6 @@
}
],
"instances": [],
"editionSettings": {},
"eventsFunctions": [
{
"fullName": "",

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