├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── new-extension.yml └── workflows │ ├── auto-pr.yml │ ├── build.yml │ ├── commands.yml │ ├── extract-translations.yml │ └── update-translations.yml ├── .gitignore ├── .gitpod.yml ├── .linguirc ├── .prettierrc ├── .translations ├── LocalesMetadata.js ├── af_ZA │ └── messages.js ├── ar_SA │ └── messages.js ├── az_AZ │ └── messages.js ├── bg_BG │ └── messages.js ├── bn_BD │ └── messages.js ├── br_FR │ └── messages.js ├── ca_ES │ └── messages.js ├── cs_CZ │ └── messages.js ├── da_DK │ └── messages.js ├── de_DE │ └── messages.js ├── el_GR │ └── messages.js ├── en │ └── messages.js ├── eo_UY │ └── messages.js ├── es_ES │ └── messages.js ├── fa_IR │ └── messages.js ├── fi_FI │ └── messages.js ├── fil_PH │ └── messages.js ├── fr_FR │ └── messages.js ├── ha_HG │ └── messages.js ├── he_IL │ └── messages.js ├── hi_IN │ └── messages.js ├── hu_HU │ └── messages.js ├── id_ID │ └── messages.js ├── ig_NG │ └── messages.js ├── it_IT │ └── messages.js ├── ja_JP │ └── messages.js ├── ka_GE │ └── messages.js ├── km_KH │ └── messages.js ├── ko_KR │ └── messages.js ├── lt_LT │ └── messages.js ├── lv_LV │ └── messages.js ├── mr_IN │ └── messages.js ├── ms_MY │ └── messages.js ├── my_MM │ └── messages.js ├── nl_NL │ └── messages.js ├── no_NO │ └── messages.js ├── pl_PL │ └── messages.js ├── pt_BR │ └── messages.js ├── pt_PT │ └── messages.js ├── ro_RO │ └── messages.js ├── ru_RU │ └── messages.js ├── si_LK │ └── messages.js ├── sk_SK │ └── messages.js ├── sl_SI │ └── messages.js ├── sq_AL │ └── messages.js ├── sr_CS │ └── messages.js ├── sr_SP │ └── messages.js ├── sv_SE │ └── messages.js ├── sw_KE │ └── messages.js ├── th_TH │ └── messages.js ├── tr_TR │ └── messages.js ├── uk_UA │ └── messages.js ├── ur_PK │ └── messages.js ├── uz_UZ │ └── messages.js ├── vi_VN │ └── messages.js ├── yo_NG │ └── messages.js ├── zh_CN │ └── messages.js └── zh_TW │ └── messages.js ├── .vscode └── settings.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── __tests__ ├── ExtensionNameValidator.spec.js ├── auto-pr │ ├── auto-pr.spec.js │ ├── test-extensions │ │ ├── community │ │ │ ├── BackButton.json │ │ │ ├── Clipboard.json │ │ │ ├── Fake.json │ │ │ └── RealExtension.json │ │ └── reviewed │ │ │ ├── ArrayTools.json │ │ │ ├── BackButton.json │ │ │ ├── Share.json │ │ │ └── UUID.json │ └── test-zips │ │ ├── empty.zip │ │ ├── invalid-zip.zip │ │ ├── not-a-json.zip │ │ ├── path-shenanigans.zip │ │ ├── too-many-extensions.zip │ │ └── valid-extension.zip └── post-build │ └── extensions.spec.js ├── crowdin.yml ├── extensions ├── community │ ├── .gitkeep │ ├── AlignObject.json │ ├── AnimationSystem.json │ ├── AudioByName.json │ ├── AudioContext.json │ ├── CameraShake3D.json │ ├── CharacterGameFeel.json │ ├── ChatBubble.json │ ├── CheatCode.json │ ├── Choose.json │ ├── Clock.json │ ├── Collision3D.json │ ├── CountdownTimer.json │ ├── CryptoApi.json │ ├── DialogBox.json │ ├── DoubleKeyPress.json │ ├── DynamicWater.json │ ├── ExtraInventory.json │ ├── FlexBox.json │ ├── FlipGravity.json │ ├── GamePixSDK.json │ ├── GamejoltAPI.json │ ├── Geolocation.json │ ├── GetPropertiesData.json │ ├── GridJump3D.json │ ├── HasLanded.json │ ├── HedgehogPlatformer.json │ ├── InkJS.json │ ├── JSONResourceLoader.json │ ├── JointConnector.json │ ├── Jump3D.json │ ├── LoadImageFromURL.json │ ├── MagneticEffect.json │ ├── MakeItRain.json │ ├── MazeGenerator.json │ ├── Model9Patch3D.json │ ├── MultitouchJoystick.json │ ├── NavMeshPathfinding.json │ ├── NewgroundsAPI.json │ ├── OllamaAI.json │ ├── PauseFocusLost.json │ ├── PlatformLedgeGrabber.json │ ├── PointAndOrbit.json │ ├── PushableAndPullableBox.json │ ├── RandomColor.json │ ├── Raycaster3D.json │ ├── RecordMovements.json │ ├── RectangularMovement.json │ ├── References.json │ ├── Reflection.json │ ├── Rotate13.json │ ├── ScreenOrientationChecker.json │ ├── SecretCode.json │ ├── SelectionTools.json │ ├── SineMovement.json │ ├── Sky3D.json │ ├── SlotSystem.json │ ├── SoundVolumeBasedOnDistance.json │ ├── Sprite3D.json │ ├── StarRating.json │ ├── Text3D.json │ ├── TextEntryConsole.json │ ├── TextEntryVirtualKeyboard.json │ ├── TimeDetector.json │ ├── TopDownCornerSliding.json │ ├── TrampolinePlatform.json │ ├── Tween3D.json │ ├── UpdateChecker.json │ ├── UploadDownloadTextFile.json │ ├── VoiceRecognition.json │ ├── Walk3D.json │ ├── WithThreeJS.json │ ├── WortalSDK.json │ └── YGameSDK.json ├── reviewed │ ├── AdvancedHTTP.json │ ├── AdvancedJump.json │ ├── AdvancedJump3D.json │ ├── AdvancedP2PEventHandling.json │ ├── AdvancedProjectile.json │ ├── AnimatedBackAndForthMovement.json │ ├── ArrayTools.json │ ├── AuthorizedPlatformsValidation.json │ ├── AutoTyping.json │ ├── BackButton.json │ ├── BaseConversion.json │ ├── BehaviorRemapper.json │ ├── Billboard.json │ ├── BoidsMovement.json │ ├── Boomerang.json │ ├── Bounce.json │ ├── ButtonStates.json │ ├── CameraImpulse.json │ ├── CameraShake.json │ ├── CameraZoom.json │ ├── CancellableDraggable.json │ ├── Checkbox.json │ ├── Checkpoints.json │ ├── Clipboard.json │ ├── ColorConversion.json │ ├── Compressor.json │ ├── CopyCameraSettings.json │ ├── CrazyGamesAdApi.json │ ├── CursorMovement.json │ ├── CursorType.json │ ├── CurvedMovement.json │ ├── DepthEffect.json │ ├── DiscordRichPresence.json │ ├── DoubleClick.json │ ├── DragCameraWithPointer.json │ ├── DraggablePhysics.json │ ├── DraggableSliderControl.json │ ├── DrawPathfinding.json │ ├── DungeonGenerator.json │ ├── EdgeScrollCamera.json │ ├── EllipseMovement.json │ ├── Emojis.json │ ├── ExplosionForce.json │ ├── ExtendedMath.json │ ├── ExtendedVariables.json │ ├── FPS.json │ ├── FaceForward.json │ ├── FireBullet.json │ ├── FirstPersonCamera.json │ ├── Flash.json │ ├── FlashLayer.json │ ├── FlashTransitionPainter.json │ ├── FollowObjectsWithCamera.json │ ├── Gamepads.json │ ├── Hash.json │ ├── Health.json │ ├── HexagonalGrid.json │ ├── HomingProjectile.json │ ├── IdleTracker.json │ ├── Iframe.json │ ├── InAppPurchase.json │ ├── InputValidation.json │ ├── InternetConnectivity.json │ ├── Inventories.json │ ├── IsOnScreen.json │ ├── KonamiCode.json │ ├── Language.json │ ├── LinearMovement.json │ ├── LinkTools.json │ ├── MQTT.json │ ├── MarchingSquares.json │ ├── MouseHelper.json │ ├── MousePointerLock.json │ ├── Noise.json │ ├── ObjectPickingTools.json │ ├── ObjectSlicer.json │ ├── ObjectSpawner.json │ ├── ObjectStack.json │ ├── OrbitingObjects.json │ ├── PanelSpriteButton.json │ ├── PanelSpriteContinuousBar.json │ ├── PanelSpriteSlider.json │ ├── Parallax.json │ ├── ParticleEmitter3D.json │ ├── PhysicsCar.json │ ├── PhysicsCar3DKeyMapper.json │ ├── PhysicsCharacter3DAnimator.json │ ├── PhysicsCharacter3DKeyMapper.json │ ├── PhysicsEllipseMovement3D.json │ ├── PinchGesture.json │ ├── PixelPerfectMovement.json │ ├── PlatformerCharacterAnimator.json │ ├── PlatformerTrajectory.json │ ├── PlayerAvatar.json │ ├── PlaygamaBridge.json │ ├── PokiGamesSDKHtml.json │ ├── PopUp.json │ ├── RTSUnitSelection.json │ ├── ReadPixels.json │ ├── Recolorizer.json │ ├── Record.json │ ├── RectangleMovement.json │ ├── RectangularFloodFill.json │ ├── RegEx.json │ ├── RenderToSprite.json │ ├── RepeatEveryXSeconds.json │ ├── RollingCounter.json │ ├── RoomBasedCameraMovement.json │ ├── ScoreCounter.json │ ├── ScreenWrap.json │ ├── ShadowClones.json │ ├── ShakeObject.json │ ├── ShakeObject3D.json │ ├── Share.json │ ├── ShockWaveEffect.json │ ├── SmoothCamera.json │ ├── SnapToGrid.json │ ├── SpeedRestrictions.json │ ├── SpriteMasking.json │ ├── SpriteMultitouchJoystick.json │ ├── SpriteSheet.json │ ├── SpriteToggleSwitch.json │ ├── StarRatingBar.json │ ├── StayOnScreen.json │ ├── Sticker.json │ ├── Sway.json │ ├── SwipeGesture.json │ ├── TextToSpeech.json │ ├── ThirdPersonCamera.json │ ├── ThreeDFlip.json │ ├── TiledUnitsBar.json │ ├── TimeFormatter.json │ ├── TimedBackAndForthMovement.json │ ├── ToggleSwitch.json │ ├── TopDownMovementAnimator.json │ ├── TravelToRandomPositions.json │ ├── Turret.json │ ├── TwoChoicesDialogBoxes.json │ ├── URLTools.json │ ├── UUID.json │ ├── UnicodeConversion.json │ ├── ValuesOfMultipleObjects.json │ ├── WebSocketClient.json │ └── YSort.json └── views.json ├── package-lock.json ├── package.json ├── scripts ├── __tests__ │ └── no-test-for-now.spec.js ├── check-single-extension.js ├── compile-translations.js ├── deploy.js ├── extract-all-translations.js ├── extract-extension.js ├── generate-extensions-registry.js ├── lib.es5.d.ts ├── lib │ ├── ExtensionNameValidator.js │ ├── ExtensionValidator.js │ ├── ExtensionsValidatorExceptions.js │ ├── Locales.js │ ├── WikiHelpLink.js │ └── rules │ │ ├── DotsInSentences.js │ │ ├── FilledOutDescriptions.js │ │ ├── HasCorrectInternalName.js │ │ ├── JavascriptDisallowedProperties.js │ │ ├── NameConsistency.js │ │ ├── NoGet.js │ │ ├── PascalCase.js │ │ ├── SemVer.js │ │ ├── _Template.js │ │ └── rule.d.ts └── types.d.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | aws-cli: circleci/aws-cli@2.0.2 5 | 6 | jobs: 7 | install: 8 | docker: 9 | - image: cimg/node:20.13.1 10 | steps: 11 | - checkout 12 | - run: npm install 13 | - persist_to_workspace: 14 | root: . 15 | paths: 16 | - node_modules 17 | tests: 18 | docker: 19 | - image: cimg/node:20.13.1 20 | steps: 21 | - checkout 22 | - attach_workspace: 23 | at: . 24 | - run: npm run check-types 25 | - run: npm test 26 | - run: npm run check-format 27 | build: 28 | docker: 29 | - image: cimg/node:20.13.1 30 | steps: 31 | - checkout 32 | - attach_workspace: 33 | at: . 34 | - run: npm run build 35 | - run: npm run check-post-build 36 | - persist_to_workspace: 37 | root: . 38 | paths: 39 | - dist 40 | deploy: 41 | docker: 42 | - image: cimg/node:20.13.1 43 | steps: 44 | - checkout 45 | - attach_workspace: 46 | at: . 47 | - aws-cli/setup 48 | - run: npm run deploy -- --cf-zoneid $CLOUDFLARE_ZONE_ID --cf-token $CLOUDFLARE_TOKEN 49 | 50 | workflows: 51 | tests: 52 | jobs: 53 | - install 54 | - build: 55 | requires: 56 | - install 57 | - tests: 58 | requires: 59 | - install 60 | - deploy: 61 | requires: 62 | - build 63 | - tests 64 | filters: 65 | branches: 66 | only: main 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛Bug report 2 | description: Create a bug report about an extension 3 | title: '[Extension name] ' 4 | labels: 5 | - '⚠ Issue with an extension' 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ⚠️ If the bug is related to GDevelop itself, go to [GDevelop Github Issues](https://github.com/4ian/GDevelop/issues) 11 | ⚠️ Thank you for taking the time and effort to report an issue! 12 | ⚠️ Please edit and complete this form before submitting: 13 | - type: checkboxes 14 | attributes: 15 | label: Is there an existing issue for this? 16 | options: 17 | - label: I have searched the [existing issues](https://github.com/GDevelopApp/GDevelop-extensions/issues) 18 | required: true 19 | - type: input 20 | attributes: 21 | label: Enter the name of the extension 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Describe the bug of the extension 27 | description: A clear and concise description of what the bug is. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Steps to reproduce 33 | description: | 34 | * Please include a link to a game if possible! 35 | * If applicable, add screenshots to help explain your problem. 36 | placeholder: | 37 | 1. Go to '...' 38 | 2. Use action '...' 39 | 3. Launch the game 40 | 4. See it's not working properly 41 | validations: 42 | required: true 43 | - type: dropdown 44 | attributes: 45 | label: GDevelop platform 46 | description: Which platform of GDevelop are you using? 47 | multiple: true 48 | options: 49 | - Desktop 50 | - Web 51 | - Mobile 52 | validations: 53 | required: true 54 | - type: input 55 | attributes: 56 | label: GDevelop version 57 | description: | 58 | Which version of GDevelop are you using? 59 | Take a look here: [Editor Home - About GDevelop - "This version of GDevelop is: ~~~"] 60 | placeholder: 5.1.159? 5.1.160? 61 | validations: 62 | required: true 63 | - type: textarea 64 | attributes: 65 | label: Platform info 66 | value: | 67 | <details> 68 | 69 | *OS (e.g. Windows, Linux, macOS, Android, iOS)* 70 | > 71 | 72 | *OS Version (e.g. Windows 10, macOS 10.15)* 73 | > 74 | 75 | *Browser(For Web) (e.g. Chrome, Firefox, Safari)* 76 | > 77 | 78 | *Device(For Mobile) (e.g. iPhone 12, Samsung Galaxy S21)* 79 | > 80 | 81 | </details> 82 | - type: textarea 83 | attributes: 84 | label: Additional context 85 | description: Add any other context about the problem here. 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: GDevelop Discord 4 | url: https://discord.gg/gdevelop 5 | about: Discuss on the forum or on the Discord to get help with an extension. 6 | - name: GDevelop Forums 7 | url: https://forum.gdevelop.io 8 | about: You can also discuss a new feature request on the forum. 9 | - name: GDevelop Extensions Trello 10 | url: https://trello.com/b/AftjL2v1/gdevelop-extensions 11 | about: See this dashboard with extension ideas and extensions being worked on by the community. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-extension.yml: -------------------------------------------------------------------------------- 1 | name: ✨New extension 2 | description: Submit your extension to be integrated in the list of community extensions 3 | title: 'New extension: <title>' 4 | labels: [✨ New extension] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: '# Extension submission' 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: Describe what your extension does here. 14 | placeholder: This extension adds... 15 | - type: textarea 16 | id: how-to 17 | attributes: 18 | label: How to use the extension 19 | description: Describe how your extension can be used. 20 | placeholder: If you put behavior X on a sprite, you can make a... 21 | - type: checkboxes 22 | id: checklist 23 | attributes: 24 | label: Checklist 25 | description: Make sure you have done all of this before submitting! 26 | options: 27 | - label: "I've followed all of [the best practices](http://wiki.compilgames.net/doku.php/gdevelop5/extensions/best-practices)." 28 | required: true 29 | - label: I confirm that this extension can be integrated to this GitHub repository, distributed and MIT licensed. 30 | required: true 31 | - label: I am aware that the extension may be updated by anyone, and do not need my explicit consent to do so. 32 | required: true 33 | - type: dropdown 34 | id: tier 35 | attributes: 36 | label: 'What tier of review do you aim for your extension?' 37 | description: '[More information](https://wiki.gdevelop.io/gdevelop5/extensions/tiers)' 38 | options: 39 | - Community (Unreviewed) 40 | - Reviewed 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: example 45 | attributes: 46 | label: Example file 47 | description: Please drag and drop an example project using your extension, compressed in a ZIP file, into this text field. **DO NOT PUT A LINK TO AN EXTERNAL SERVICE LIKE GOOGLE DRIVE!** 48 | placeholder: '[MyExample.zip]()' 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: extension 53 | attributes: 54 | label: Extension file 55 | description: Please drag and drop your extension JSON file, compressed in a ZIP file, into this text field. **DO NOT PUT A LINK TO AN EXTERNAL SERVICE LIKE GOOGLE DRIVE!** 56 | placeholder: '[MyExtension.json.zip]()' 57 | validations: 58 | required: true 59 | - type: markdown 60 | attributes: 61 | value: | 62 | You also may have to create an account on GitHub before posting. 63 | Your extension will be added to the list after we have checked that it contains no virus and respects the best practices. 64 | Thanks for contributing to GDevelop! 🙌 65 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build of the database 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | jobs: 6 | rebuild-database: 7 | # Only run it on extensions submissions (tag is ensured via the new issue form) 8 | if: ${{ contains(github.event.pull_request.labels.*.name, '✨ New extension') || contains(github.event.pull_request.labels.*.name, '🔄 Extension update') }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Rebuild the database 14 | run: | 15 | npm install 16 | echo 'BUILD_LOGS<<EOF' >> $GITHUB_ENV 17 | node scripts/generate-extensions-registry.js --disable-exit-code >> $GITHUB_ENV 18 | echo 'EOF' >> $GITHUB_ENV 19 | 20 | - name: Notify build errors 21 | if: ${{ !contains(env.BUILD_LOGS, 'successfully updated') }} 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: | 25 | gh pr comment ${{ github.event.pull_request.number }} --body "Errors were detected in this submission: 26 | 27 | \`\`\`${{ env.BUILD_LOGS }} 28 | \`\`\`" 29 | -------------------------------------------------------------------------------- /.github/workflows/commands.yml: -------------------------------------------------------------------------------- 1 | name: GDevelop Extensions PR commands 2 | on: 3 | issue_comment: 4 | types: [created, edited] 5 | 6 | jobs: 7 | merge: 8 | runs-on: ubuntu-latest 9 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '!merge') }} 10 | steps: 11 | - uses: xt0rted/pull-request-comment-branch@v2 12 | id: comment-branch 13 | 14 | - uses: actions/checkout@v3 15 | with: 16 | ref: ${{ steps.comment-branch.outputs.head_ref }} 17 | fetch-depth: 0 18 | 19 | - name: Merge new commits from main 20 | run: | 21 | git config --global user.name '${{ github.actor }}' 22 | git config --global user.email '${{ github.actor }}@users.noreply.github.com' 23 | git merge -X theirs origin/main -m "Merge main into this branch" 24 | 25 | - name: Rebuild the database 26 | run: | 27 | npm i 28 | echo 'BUILD_LOGS<<EOF' >> $GITHUB_ENV 29 | node scripts/generate-extensions-registry.js --disable-exit-code >> $GITHUB_ENV 30 | echo 'EOF' >> $GITHUB_ENV 31 | 32 | - name: Commit and push 33 | if: ${{ contains(env.BUILD_LOGS, 'successfully updated') }} 34 | run: | 35 | git push 36 | 37 | - name: Notify success 38 | if: ${{ contains(env.BUILD_LOGS, 'successfully updated') }} 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | run: gh pr comment ${{ github.event.issue.number }} --body "✅ Successfully merged main into this branch." 42 | 43 | - name: Notify error 44 | if: ${{ !contains(env.BUILD_LOGS, 'successfully updated') }} 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: | 48 | gh pr comment ${{ github.event.issue.number }} --body "A build error occured while merging: 49 | 50 | \`\`\`${{ env.BUILD_LOGS }} 51 | \`\`\`" 52 | 53 | - name: Notify any error 54 | if: ${{ failure() }} 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | run: | 58 | gh pr comment ${{ github.event.issue.number }} --body "❗ An internal error has occurred. See logs at https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}." 59 | 60 | update: 61 | runs-on: ubuntu-latest 62 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '!update') && !contains(github.event.comment.body, 'Uploading') }} 63 | steps: 64 | - uses: xt0rted/pull-request-comment-branch@v2 65 | id: comment-branch 66 | 67 | - uses: actions/checkout@v3 68 | with: 69 | ref: ${{ steps.comment-branch.outputs.head_ref }} 70 | 71 | - name: Download extension 72 | env: 73 | BODY: ${{ github.event.comment.body }} 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: | 76 | FILEURL=$(echo $BODY | grep -ioEh 'https?://\S+.zip' -m1) 77 | curl -L $FILEURL -o /tmp/ext.zip 78 | unzip -o /tmp/ext.zip -d ./extensions/community 79 | 80 | - name: Rebuild the database 81 | run: | 82 | npm i 83 | echo 'BUILD_LOGS<<EOF' >> $GITHUB_ENV 84 | node scripts/generate-extensions-registry.js --disable-exit-code >> $GITHUB_ENV 85 | echo 'EOF' >> $GITHUB_ENV 86 | 87 | - name: Setup git config 88 | run: | 89 | git config --global user.name '${{ github.actor }}' 90 | git config --global user.email '${{ github.actor }}@users.noreply.github.com' 91 | 92 | - name: Check for changed file 93 | id: git-diff 94 | run: | 95 | git diff --quiet || echo "changed=true" >> $GITHUB_OUTPUT 96 | shell: bash 97 | 98 | - name: Commit and push 99 | if: ${{ steps.git-diff.outputs.changed == 'true' && contains(env.BUILD_LOGS, 'successfully updated') }} 100 | run: | 101 | git add extensions 102 | git commit -m "Updated extension" 103 | git push 104 | 105 | - name: Notify no changed 106 | if: ${{ steps.git-diff.outputs.changed != 'true' }} 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | run: gh pr comment ${{ github.event.issue.number }} --body "❗ No updates found. Please check your file." 110 | 111 | - name: Notify success 112 | if: ${{ steps.git-diff.outputs.changed == 'true' && contains(env.BUILD_LOGS, 'successfully updated') }} 113 | env: 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | run: gh pr comment ${{ github.event.issue.number }} --body "✅ Successfully updated the extension." 116 | 117 | - name: Notify build error 118 | if: ${{ steps.git-diff.outputs.changed == 'true' && !contains(env.BUILD_LOGS, 'successfully updated') }} 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | run: | 122 | gh pr comment ${{ github.event.issue.number }} --body "Can't update the extension, as it doesn't pass automatic tests: 123 | 124 | \`\`\`${{ env.BUILD_LOGS }} 125 | \`\`\`" 126 | 127 | - name: Notify any error 128 | if: ${{ failure() }} 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | run: | 132 | gh pr comment ${{ github.event.issue.number }} --body "❗ An internal error has occurred. See logs at https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}." 133 | -------------------------------------------------------------------------------- /.github/workflows/extract-translations.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action to extract translations and (later) upload them to Crowdin. 2 | 3 | name: Extract translations 4 | on: 5 | # Execute for all commits (to ensure translations extraction works) 6 | push: 7 | branches: 8 | - '**' 9 | tags-ignore: 10 | - '**' # Don't run on new tags 11 | # Allows to run this workflow manually from the Actions tab. 12 | workflow_dispatch: 13 | 14 | jobs: 15 | extract-translations: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | cache: 'npm' 23 | cache-dependency-path: 'package-lock.json' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Extract translations 29 | run: npm run extract-all-translations 30 | 31 | # Only upload on Crowdin for the main branch 32 | - name: Install Crowdin CLI 33 | if: github.ref == 'refs/heads/main' 34 | run: npm i -g @crowdin/cli 35 | 36 | - name: Upload translations to Crowdin 37 | run: crowdin upload sources 38 | if: github.ref == 'refs/heads/main' 39 | env: 40 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 41 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/update-translations.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action to update translations by downloading them from Crowdin, 2 | # and open a Pull Request with the changes on GDevelop's repository. 3 | 4 | name: Update translations 5 | on: 6 | # Execute only on main 7 | push: 8 | branches: 9 | - main 10 | tags-ignore: 11 | - '**' # Don't run on new tags 12 | # Allows to run this workflow manually from the Actions tab. 13 | workflow_dispatch: 14 | 15 | jobs: 16 | update-translations: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 20 23 | cache: 'npm' 24 | cache-dependency-path: 'package-lock.json' 25 | 26 | - name: Install gettext 27 | run: sudo apt update && sudo apt install gettext -y 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | # (Build and) download the most recent translations (PO files) from Crowdin. 33 | - name: Install Crowdin CLI 34 | run: npm i -g @crowdin/cli 35 | 36 | - name: Extract translations 37 | run: npm run extract-all-translations 38 | 39 | - name: Download new translations from Crowdin 40 | run: crowdin download 41 | env: 42 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 43 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 44 | 45 | # Seems like the three letters code is not handled properly by LinguiJS? 46 | # Do without this language while we find a solution. 47 | - name: Remove catalogs not handled properly by LinguiJS compile command. 48 | run: rm -rf .translations/pcm_NG/ 49 | 50 | - name: Compile translations into .js files that are read by LinguiJS (for the editor) 51 | run: npm run compile-translations 52 | 53 | - name: Create a Pull Request with the changes 54 | uses: peter-evans/create-pull-request@v6 55 | with: 56 | commit-message: Update translations [skip ci] 57 | branch: chore/update-translations 58 | delete-branch: true 59 | title: '[Auto PR] Update translations' 60 | body: | 61 | This updates the translations by downloading them from Crowdin and compiling them for usage by the app. 62 | 63 | Please double check the values in `.translations/LocalesMetadata.js` to ensure the changes are sensible. 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | *.po 5 | *.pot -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | tasks: 6 | - init: npm install && npm run build 7 | 8 | 9 | -------------------------------------------------------------------------------- /.linguirc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLocale": "en", 3 | "localeDir": ".translations/", 4 | "format": "po", 5 | "pseudoLocale": "pseudo_LOCALE" 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Non-extension files should be reviewed by the core team 2 | * @GDevelopApp/core-engineering-team 3 | 4 | # Extensions are to be reviewed by the extensions team 5 | /extensions/ @GDevelopApp/extensions-team 6 | 7 | # Modifications to the rules for submitting extension should be reviewed 8 | # by both the core team and the extensions team 9 | /scripts/lib/rules/ @GDevelopApp/core-engineering-team @GDevelopApp/extensions-team 10 | /scripts/lib/ExtensionsValidatorExceptions.js @GDevelopApp/core-engineering-team @GDevelopApp/extensions-team 11 | /scripts/lib/ExtensionValidator.js @GDevelopApp/core-engineering-team @GDevelopApp/extensions-team 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Florian Rival 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GDevelop logo](https://raw.githubusercontent.com/4ian/GDevelop/master/newIDE/GDevelop%20banner.png 'GDevelop logo') 2 | 3 | GDevelop is a **full-featured, no-code, open-source** game development software. You can build **2D, 3D and multiplayer games** for mobile (iOS, Android), desktop and the web. GDevelop is fast and easy to use: the game logic is built up using an intuitive and powerful event-based system and reusable behaviors. 4 | 5 | # GDevelop Official and Community Extensions 6 | 7 | This repository hosts community made extensions for GDevelop. Extensions can provide new behaviors for objects, actions, conditions or expressions. 8 | 9 | ## Getting started 10 | 11 | | ❔ I want to... | 🚀 What to do | 12 | | ------------------------------- | --------------------------------------------------------------------- | 13 | | 🎮 Use GDevelop to make games | Go to [GDevelop homepage](https://gdevelop.io) to download the app! | 14 | | Use an extension | Extensions can be **searched and downloaded** directly from GDevelop. | 15 | | Contribute to GDevelop itself | Visit [GDevelop GitHub repository](https://github.com/4ian/GDevelop). | 16 | | Create/improve an extension | Read below. | 17 | 18 | ## Submit your extension to community extensions 19 | 20 | If you've created an extension with GDevelop, you can submit it to be shared with the rest of the community. This [Trello board has the extensions that are being worked on](https://trello.com/b/AftjL2v1/gdevelop-extensions) by the community. 21 | 22 | 1. **Create your extension** in your game with GDevelop: see the documentation about [functions](https://wiki.gdevelop.io/gdevelop5/events/functions) and [custom behaviors](https://wiki.gdevelop.io/gdevelop5/behaviors/events-based-behaviors). 23 | 2. Make sure the **descriptions**, **tags**, **names** are properly filled in the options of the extension. 24 | 3. **Export** your extension to a _.json file_ from GDevelop. 25 | 4. Submit it! You can either: 26 | - **Easy**: [submit it here](https://github.com/4ian/GDevelop-extensions/issues/new/choose), attaching the _.json file_ (as a zip, because GitHub won't accept json files directly). 27 | - **If you know how to use git**: fork this repository, clone the git, add your .json file in `extensions/community` folder. Finally [open a Pull Request](https://github.com/4ian/GDevelop-extensions/compare). 28 | 5. Your extension will be added after a automated checks and a _quick safety check_. 🚀 29 | 30 | > **Note**: If automated checks are failing, please adapt your extension and submit it again to get it added! Even if we don't do a full review of all extensions, just safety checks, the automated checks must pass. Look at automated comments that will be added to the _Pull Request_ corresponding to your submission. 31 | 32 | ## Get your extension (reviewed extensions) 33 | 34 | Reviewed extensions are community extensions that got reviewed and adapted to meet all **[the best practices that are listed here](https://wiki.gdevelop.io/gdevelop5/extensions/best-practices)**. 35 | 36 | If your community extension is very useful and you think its quality is good enough: 37 | 38 | 1. open a _Pull Request_ to move it from the `community` folder to the `reviewed` folder. 39 | 2. A member of the _GDevelop Extensions Team_ will then review it and give you feedback on what to do to have it reviewed. 40 | 3. When it's ready, it will be merged and the extension now accessible in the "reviewed" extensions. 41 | 42 | > **Note**: When your extension gets reviewed, the extension team will ask you to adapt your extension to reach a fairly high quality bar. It's normal! The feedback is here to help get the extension in a state where it's super flexible and useful for all users. 43 | 44 | ## License 45 | 46 | All extensions provided on this repository are MIT licensed. 47 | -------------------------------------------------------------------------------- /__tests__/ExtensionNameValidator.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | isValidExtensionName, 3 | } = require('../scripts/lib/ExtensionNameValidator'); 4 | 5 | describe('ExtensionNameValidator.js', () => { 6 | test('Disallows invalid names', () => { 7 | expect(isValidExtensionName('')).toBeFalsy(); 8 | expect(isValidExtensionName(' ')).toBeFalsy(); 9 | expect(isValidExtensionName('!')).toBeFalsy(); 10 | expect(isValidExtensionName('😀')).toBeFalsy(); 11 | 12 | expect(isValidExtensionName('hello')).toBeFalsy(); 13 | expect(isValidExtensionName('1hello')).toBeFalsy(); 14 | expect(isValidExtensionName('Hello!')).toBeFalsy(); 15 | expect(isValidExtensionName('!Hello')).toBeFalsy(); 16 | expect(isValidExtensionName('He!!o')).toBeFalsy(); 17 | expect(isValidExtensionName('Hello:')).toBeFalsy(); 18 | expect(isValidExtensionName('Hello@')).toBeFalsy(); 19 | expect(isValidExtensionName('Hello/')).toBeFalsy(); 20 | expect(isValidExtensionName('Hello ')).toBeFalsy(); 21 | expect(isValidExtensionName('Hello[')).toBeFalsy(); 22 | expect(isValidExtensionName('Hello`')).toBeFalsy(); 23 | expect(isValidExtensionName('Hello{')).toBeFalsy(); 24 | expect(isValidExtensionName('Hello😀')).toBeFalsy(); 25 | }); 26 | 27 | test('Allows valid names', () => { 28 | expect(isValidExtensionName('Hello')).toBeTruthy(); 29 | expect(isValidExtensionName('HelloWorld')).toBeTruthy(); 30 | expect(isValidExtensionName('HelloWorld12')).toBeTruthy(); 31 | expect(isValidExtensionName('P2P')).toBeTruthy(); 32 | expect(isValidExtensionName('Rotate13')).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/auto-pr/auto-pr.spec.js: -------------------------------------------------------------------------------- 1 | const { mkdir, rm } = require('fs/promises'); 2 | const { verifyExtension } = require('../../scripts/check-single-extension'); 3 | const { extractExtension } = require('../../scripts/extract-extension'); 4 | 5 | const TEMPORARY_MOCK_EXTENSIONS_FOLDER = __dirname + '/mock_extensions_folder'; 6 | const TEST_ZIPS_FOLDER = __dirname + '/test-zips'; 7 | const TEST_EXTENSIONS_FOLDER = __dirname + '/test-extensions'; 8 | 9 | /** @param {string} zipName */ 10 | const wrappedExtractExtension = async (zipName) => 11 | ( 12 | await extractExtension( 13 | `${TEST_ZIPS_FOLDER}/${zipName}.zip`, 14 | TEMPORARY_MOCK_EXTENSIONS_FOLDER 15 | ) 16 | ).error; 17 | 18 | /** @param {string} extensionName */ 19 | const wrappedVerifyExtension = async (extensionName) => 20 | ( 21 | await verifyExtension(extensionName, { 22 | extensionsFolder: TEST_EXTENSIONS_FOLDER, 23 | }) 24 | ).code; 25 | 26 | describe('Auto-pr pipeline', () => { 27 | beforeAll(async () => 28 | mkdir(TEMPORARY_MOCK_EXTENSIONS_FOLDER + '/community', { recursive: true }) 29 | ); 30 | beforeAll(async () => 31 | mkdir(TEMPORARY_MOCK_EXTENSIONS_FOLDER + '/reviewed', { recursive: true }) 32 | ); 33 | afterAll(async () => 34 | rm(TEMPORARY_MOCK_EXTENSIONS_FOLDER, { recursive: true }) 35 | ); 36 | 37 | test('extractExtension()', async () => { 38 | expect(await wrappedExtractExtension(`empty`)).toBe('no-json-found'); 39 | expect(await wrappedExtractExtension(`invalid-zip`)).toBe('zip-error'); 40 | expect(await wrappedExtractExtension(`not-a-json`)).toBe('no-json-found'); 41 | expect(await wrappedExtractExtension(`path-shenanigans`)).toBe( 42 | 'invalid-file-name' 43 | ); 44 | expect(await wrappedExtractExtension(`too-many-extensions`)).toBe( 45 | 'too-many-files' 46 | ); 47 | 48 | expect(await wrappedExtractExtension(`valid-extension`)).toBeUndefined(); 49 | }); 50 | 51 | test(`verifyExtension()`, async () => { 52 | expect(await wrappedVerifyExtension(`NonExisting`)).toBe('not-found'); 53 | expect(await wrappedVerifyExtension(`BackButton`)).toBe('duplicated'); 54 | expect( 55 | await wrappedVerifyExtension(`../../../../../../../etc/passwd`) 56 | ).toBe('invalid-file-name'); 57 | expect(await wrappedVerifyExtension(`cri.png`)).toBe('invalid-file-name'); 58 | expect(await wrappedVerifyExtension(`RealExtension`)).toBe('invalid-json'); 59 | expect(await wrappedVerifyExtension(`Share`)).toBe('rule-break'); 60 | expect(await wrappedVerifyExtension(`Fake`)).toBe('unknown-json-contents'); 61 | expect(await wrappedVerifyExtension(`ArrayTools`)).toBe('gdevelop-project-file'); 62 | 63 | expect(await wrappedVerifyExtension(`UUID`)).toBe('success'); 64 | expect(await wrappedVerifyExtension(`Clipboard`)).toBe('success'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/community/BackButton.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "description": "Prevents the back button from quitting the game and provides a condition to check when it's pressed (to allow customising its behavior).", 4 | "extensionNamespace": "", 5 | "fullName": "Back button", 6 | "helpPath": "", 7 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWtleWJvYXJkLWJhY2tzcGFjZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0yMSwxMUg2LjgzTDEwLjQxLDcuNDFMOSw2TDMsMTJMOSwxOEwxMC40MSwxNi41OEw2LjgzLDEzSDIxVjExWiIgLz48L3N2Zz4=", 8 | "name": "BackButton", 9 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/keyboard-backspace.svg", 10 | "shortDescription": "Adds interactions with the back button.", 11 | "version": "1.0.0", 12 | "tags": [ 13 | "back", 14 | "mobile", 15 | "button", 16 | "input" 17 | ], 18 | "authorIds": [ 19 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 20 | ], 21 | "dependencies": [], 22 | "eventsFunctions": [ 23 | { 24 | "description": "", 25 | "fullName": "", 26 | "functionType": "Action", 27 | "name": "onFirstSceneLoaded", 28 | "private": false, 29 | "sentence": "", 30 | "events": [ 31 | { 32 | "disabled": false, 33 | "folded": false, 34 | "type": "BuiltinCommonInstructions::JsCode", 35 | "inlineCode": "gdjs.evtTools.back_button = {\n triggered: false,\n _popStateListener: (event) => {\n gdjs.evtTools.back_button.triggered = true;\n history.pushState(\"\", \"\"); // Push a new fake state as the old one was popped\n }\n};\n\n// Handle back button on the web\nhistory.pushState(\"\", \"\"); // Push a fake state to prevent switching page when clicking on back\nwindow.addEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n\n// Handle back button on cordova\ndocument.addEventListener(\"backbutton\", e => {\n e.preventDefault();\n gdjs.evtTools.back_button.triggered = true;\n}, false); \n", 36 | "parameterObjects": "", 37 | "useStrict": true, 38 | "eventsSheetExpanded": false 39 | } 40 | ], 41 | "parameters": [], 42 | "objectGroups": [] 43 | }, 44 | { 45 | "description": "Triggers whenever the player presses the back button.", 46 | "fullName": "Back button is pressed", 47 | "functionType": "Condition", 48 | "name": "onBackButtonPressed", 49 | "private": false, 50 | "sentence": "Back button is pressed", 51 | "events": [ 52 | { 53 | "disabled": false, 54 | "folded": false, 55 | "type": "BuiltinCommonInstructions::JsCode", 56 | "inlineCode": "eventsFunctionContext.returnValue = gdjs.evtTools.back_button.triggered;\n", 57 | "parameterObjects": "", 58 | "useStrict": true, 59 | "eventsSheetExpanded": false 60 | } 61 | ], 62 | "parameters": [], 63 | "objectGroups": [] 64 | }, 65 | { 66 | "description": "This simulates the normal action of the back button. \nThis action will quit the app when in a mobile app, and go back to the previous page when in a web browser.", 67 | "fullName": "Trigger back button", 68 | "functionType": "Action", 69 | "name": "doDefault", 70 | "private": false, 71 | "sentence": "Simulate back button press", 72 | "events": [ 73 | { 74 | "disabled": false, 75 | "folded": false, 76 | "type": "BuiltinCommonInstructions::JsCode", 77 | "inlineCode": "// Close the app on cordova, as this is the default behavior\nif (navigator.app) {\n navigator.app.exitApp();\n} else if (navigator.device && navigator.device.exitApp) {\n navigator.device.exitApp();\n} else {\n // Go to previous page as it is the default on browsers\n // Remove the listener so new fake states don't get pushed\n window.removeEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n history.back(); // Remove the state that prevents going back\n history.back(); // Actually go back\n}\n", 78 | "parameterObjects": "", 79 | "useStrict": true, 80 | "eventsSheetExpanded": false 81 | } 82 | ], 83 | "parameters": [], 84 | "objectGroups": [] 85 | }, 86 | { 87 | "description": "", 88 | "fullName": "", 89 | "functionType": "Action", 90 | "name": "onScenePostEvents", 91 | "private": false, 92 | "sentence": "", 93 | "events": [ 94 | { 95 | "disabled": false, 96 | "folded": false, 97 | "type": "BuiltinCommonInstructions::JsCode", 98 | "inlineCode": "gdjs.evtTools.back_button.triggered = false;\n", 99 | "parameterObjects": "", 100 | "useStrict": true, 101 | "eventsSheetExpanded": false 102 | } 103 | ], 104 | "parameters": [], 105 | "objectGroups": [] 106 | } 107 | ], 108 | "eventsBasedBehaviors": [] 109 | } -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/community/Fake.json: -------------------------------------------------------------------------------- 1 | { 2 | "link": "https://www.youtube.com/watch?v=xvFZjo5PgG0", 3 | "rating": 5 4 | } -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/community/RealExtension.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/__tests__/auto-pr/test-extensions/community/RealExtension.json -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/reviewed/BackButton.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "description": "Prevents the back button from quitting the game and provides a condition to check when it's pressed (to allow customising its behavior).", 4 | "extensionNamespace": "", 5 | "fullName": "Back button", 6 | "helpPath": "", 7 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWtleWJvYXJkLWJhY2tzcGFjZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0yMSwxMUg2LjgzTDEwLjQxLDcuNDFMOSw2TDMsMTJMOSwxOEwxMC40MSwxNi41OEw2LjgzLDEzSDIxVjExWiIgLz48L3N2Zz4=", 8 | "name": "BackButton", 9 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/keyboard-backspace.svg", 10 | "shortDescription": "Adds interactions with the back button.", 11 | "version": "1.0.0", 12 | "tags": [ 13 | "back", 14 | "mobile", 15 | "button", 16 | "input" 17 | ], 18 | "authorIds": [ 19 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 20 | ], 21 | "dependencies": [], 22 | "eventsFunctions": [ 23 | { 24 | "description": "", 25 | "fullName": "", 26 | "functionType": "Action", 27 | "name": "onFirstSceneLoaded", 28 | "private": false, 29 | "sentence": "", 30 | "events": [ 31 | { 32 | "disabled": false, 33 | "folded": false, 34 | "type": "BuiltinCommonInstructions::JsCode", 35 | "inlineCode": "gdjs.evtTools.back_button = {\n triggered: false,\n _popStateListener: (event) => {\n gdjs.evtTools.back_button.triggered = true;\n history.pushState(\"\", \"\"); // Push a new fake state as the old one was popped\n }\n};\n\n// Handle back button on the web\nhistory.pushState(\"\", \"\"); // Push a fake state to prevent switching page when clicking on back\nwindow.addEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n\n// Handle back button on cordova\ndocument.addEventListener(\"backbutton\", e => {\n e.preventDefault();\n gdjs.evtTools.back_button.triggered = true;\n}, false); \n", 36 | "parameterObjects": "", 37 | "useStrict": true, 38 | "eventsSheetExpanded": false 39 | } 40 | ], 41 | "parameters": [], 42 | "objectGroups": [] 43 | }, 44 | { 45 | "description": "Triggers whenever the player presses the back button.", 46 | "fullName": "Back button is pressed", 47 | "functionType": "Condition", 48 | "name": "onBackButtonPressed", 49 | "private": false, 50 | "sentence": "Back button is pressed", 51 | "events": [ 52 | { 53 | "disabled": false, 54 | "folded": false, 55 | "type": "BuiltinCommonInstructions::JsCode", 56 | "inlineCode": "eventsFunctionContext.returnValue = gdjs.evtTools.back_button.triggered;\n", 57 | "parameterObjects": "", 58 | "useStrict": true, 59 | "eventsSheetExpanded": false 60 | } 61 | ], 62 | "parameters": [], 63 | "objectGroups": [] 64 | }, 65 | { 66 | "description": "This simulates the normal action of the back button. \nThis action will quit the app when in a mobile app, and go back to the previous page when in a web browser.", 67 | "fullName": "Trigger back button", 68 | "functionType": "Action", 69 | "name": "doDefault", 70 | "private": false, 71 | "sentence": "Simulate back button press", 72 | "events": [ 73 | { 74 | "disabled": false, 75 | "folded": false, 76 | "type": "BuiltinCommonInstructions::JsCode", 77 | "inlineCode": "// Close the app on cordova, as this is the default behavior\nif (navigator.app) {\n navigator.app.exitApp();\n} else if (navigator.device && navigator.device.exitApp) {\n navigator.device.exitApp();\n} else {\n // Go to previous page as it is the default on browsers\n // Remove the listener so new fake states don't get pushed\n window.removeEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n history.back(); // Remove the state that prevents going back\n history.back(); // Actually go back\n}\n", 78 | "parameterObjects": "", 79 | "useStrict": true, 80 | "eventsSheetExpanded": false 81 | } 82 | ], 83 | "parameters": [], 84 | "objectGroups": [] 85 | }, 86 | { 87 | "description": "", 88 | "fullName": "", 89 | "functionType": "Action", 90 | "name": "onScenePostEvents", 91 | "private": false, 92 | "sentence": "", 93 | "events": [ 94 | { 95 | "disabled": false, 96 | "folded": false, 97 | "type": "BuiltinCommonInstructions::JsCode", 98 | "inlineCode": "gdjs.evtTools.back_button.triggered = false;\n", 99 | "parameterObjects": "", 100 | "useStrict": true, 101 | "eventsSheetExpanded": false 102 | } 103 | ], 104 | "parameters": [], 105 | "objectGroups": [] 106 | } 107 | ], 108 | "eventsBasedBehaviors": [] 109 | } -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/reviewed/Share.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "description": "Share stuff", 4 | "extensionNamespace": "", 5 | "fullName": "Share", 6 | "helpPath": "", 7 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXNoYXJlLXZhcmlhbnQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTgsMTYuMDhDMTcuMjQsMTYuMDggMTYuNTYsMTYuMzggMTYuMDQsMTYuODVMOC45MSwxMi43QzguOTYsMTIuNDcgOSwxMi4yNCA5LDEyQzksMTEuNzYgOC45NiwxMS41MyA4LjkxLDExLjNMMTUuOTYsNy4xOUMxNi41LDcuNjkgMTcuMjEsOCAxOCw4QTMsMyAwIDAsMCAyMSw1QTMsMyAwIDAsMCAxOCwyQTMsMyAwIDAsMCAxNSw1QzE1LDUuMjQgMTUuMDQsNS40NyAxNS4wOSw1LjdMOC4wNCw5LjgxQzcuNSw5LjMxIDYuNzksOSA2LDlBMywzIDAgMCwwIDMsMTJBMywzIDAgMCwwIDYsMTVDNi43OSwxNSA3LjUsMTQuNjkgOC4wNCwxNC4xOUwxNS4xNiwxOC4zNEMxNS4xMSwxOC41NSAxNS4wOCwxOC43NyAxNS4wOCwxOUMxNS4wOCwyMC42MSAxNi4zOSwyMS45MSAxOCwyMS45MUMxOS42MSwyMS45MSAyMC45MiwyMC42MSAyMC45MiwxOUEyLjkyLDIuOTIgMCAwLDAgMTgsMTYuMDhaIiAvPjwvc3ZnPg==", 8 | "name": "Share", 9 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/share-variant.svg", 10 | "shortDescription": "Allows to share content via the system share dialog", 11 | "version": "0.0.1", 12 | "tags": [], 13 | "dependencies": [ 14 | { 15 | "exportName": "cordova-plugin-web-share", 16 | "name": "New Dependency", 17 | "type": "cordova", 18 | "version": "https://github.com/arthuro555/cordova-webshare-api.git" 19 | } 20 | ], 21 | "eventsFunctions": [ 22 | { 23 | "description": "Share some data via another app using the system share dialog.", 24 | "fullName": "Share", 25 | "functionType": "Action", 26 | "name": "Share", 27 | "private": false, 28 | "sentence": "Share _PARAM1_/_PARAM2_ with title _PARAM3_ (callback variable: _PARAM4_)", 29 | "events": [ 30 | { 31 | "disabled": false, 32 | "folded": false, 33 | "type": "BuiltinCommonInstructions::JsCode", 34 | "inlineCode": "const getParam = (name) =>\r\n eventsFunctionContext.getArgument(name).lenght === 0\r\n ? undefined\r\n : eventsFunctionContext.getArgument(name);\r\n\r\nconst statusCb = runtimeScene.getVariables().get(eventsFunctionContext.getArgument(\"status\"));\r\n\r\nif (navigator.share) \r\n navigator.share({\r\n title: getParam(\"title\"),\r\n text: getParam(\"text\"),\r\n url: getParam(\"url\"),\r\n // Not adding files as GDevelop doesn't has real support for those.\r\n })\r\n .then(() => statusCb.setString(\"ok\"))\r\n .catch(() => statusCb.setString(\"canceled\"));\r\nelse statusCb.setString(\"unsupported\")\r\n", 35 | "parameterObjects": "", 36 | "useStrict": true, 37 | "eventsSheetExpanded": false 38 | } 39 | ], 40 | "parameters": [ 41 | { 42 | "codeOnly": false, 43 | "defaultValue": "", 44 | "description": "A text to share", 45 | "longDescription": "", 46 | "name": "text", 47 | "optional": false, 48 | "supplementaryInformation": "", 49 | "type": "string" 50 | }, 51 | { 52 | "codeOnly": false, 53 | "defaultValue": "", 54 | "description": "A url to share", 55 | "longDescription": "", 56 | "name": "url", 57 | "optional": false, 58 | "supplementaryInformation": "", 59 | "type": "string" 60 | }, 61 | { 62 | "codeOnly": false, 63 | "defaultValue": "", 64 | "description": "A title to show in the share dialog", 65 | "longDescription": "", 66 | "name": "title", 67 | "optional": false, 68 | "supplementaryInformation": "", 69 | "type": "string" 70 | }, 71 | { 72 | "codeOnly": false, 73 | "defaultValue": "", 74 | "description": "Status callback scene variable name", 75 | "longDescription": "Will be set to `ok` if shared, `canceled` if the user canceled the share or `unsupported`.", 76 | "name": "status", 77 | "optional": false, 78 | "supplementaryInformation": "", 79 | "type": "string" 80 | } 81 | ], 82 | "objectGroups": [] 83 | } 84 | ], 85 | "eventsBasedBehaviors": [] 86 | } -------------------------------------------------------------------------------- /__tests__/auto-pr/test-extensions/reviewed/UUID.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "description": "Adds UUID (Universally Unique Identifiers) generation expressions via multiple patterns:\n- UUIDv4: Creates a long random string of characters following the UUIDv4 standard. If available on the system/browser, will use a cryptographic random number generator, otherwise uses the same pseudorandom number generator as the `Random()` expression. Chances of collisions are extremely low, but not non-existent. As the return value is a string, it may not be the most performant pattern. It can not be predicted in most cases.\n- Incremented integer: Returns use an integer that will be incremented after each call. Very performant and no risk of collisions. The UID will be predictable, so it may be vulnerable to some cryptographic attacks if used for private unique tokens, like password reset verification UID. Note that if you store IDs and then restart the game, there may be duplicates, since it'll reset the counter.", 4 | "extensionNamespace": "", 5 | "fullName": "Unique Identifiers", 6 | "helpPath": "", 7 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWlkZW50aWZpZXIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNiwxOFY4SDhWMThINk02LDQuNUg4VjYuNUg2VjQuNU0xNyw0SDE5VjE4SDE3VjE3Ljc1QzE3LDE3Ljc1IDE1LjY3LDE4IDE1LDE4QTUsNSAwIDAsMSAxMCwxM0E1LDUgMCAwLDEgMTUsOEMxNS42Nyw4IDE3LDguMjUgMTcsOC4yNVY0TTE3LDEwLjI1QzE3LDEwLjI1IDE1LjY3LDEwIDE1LDEwQTMsMyAwIDAsMCAxMiwxM0EzLDMgMCAwLDAgMTUsMTZDMTUuNjcsMTYgMTcsMTUuNzUgMTcsMTUuNzVWMTAuMjVaIiAvPjwvc3ZnPg==", 8 | "name": "UUID", 9 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/identifier.svg", 10 | "shortDescription": "A collection of UID generation expressions.", 11 | "version": "1.0.0", 12 | "tags": [ 13 | "random", 14 | "generation", 15 | "uid", 16 | "uuid", 17 | "guid", 18 | "v4", 19 | "unique", 20 | "id", 21 | "identifier" 22 | ], 23 | "dependencies": [], 24 | "eventsFunctions": [ 25 | { 26 | "description": "Generates a unique identifier with the UUIDv4 pattern.", 27 | "fullName": "Generate a UUIDv4", 28 | "functionType": "StringExpression", 29 | "name": "GenerateUUIDv4", 30 | "private": false, 31 | "sentence": "", 32 | "events": [ 33 | { 34 | "disabled": false, 35 | "folded": false, 36 | "type": "BuiltinCommonInstructions::JsCode", 37 | "inlineCode": "// Use the engine implementation of UUIDv4.\neventsFunctionContext.returnValue = gdjs.makeUuid();\n", 38 | "parameterObjects": "", 39 | "useStrict": true, 40 | "eventsSheetExpanded": false 41 | } 42 | ], 43 | "parameters": [], 44 | "objectGroups": [] 45 | }, 46 | { 47 | "description": "Generates a unique identifier with the incremented integer pattern.", 48 | "fullName": "Generate an incremented integer UID", 49 | "functionType": "Expression", 50 | "name": "GenerateIncrementedIntegerUID", 51 | "private": false, 52 | "sentence": "", 53 | "events": [ 54 | { 55 | "disabled": false, 56 | "folded": false, 57 | "type": "BuiltinCommonInstructions::Standard", 58 | "conditions": [], 59 | "actions": [ 60 | { 61 | "type": { 62 | "inverted": false, 63 | "value": "SetReturnNumber" 64 | }, 65 | "parameters": [ 66 | "GlobalVariable(__UUID_IncrementedInteger)" 67 | ], 68 | "subInstructions": [] 69 | }, 70 | { 71 | "type": { 72 | "inverted": false, 73 | "value": "ModVarGlobal" 74 | }, 75 | "parameters": [ 76 | "__UUID_IncrementedInteger", 77 | "+", 78 | "1" 79 | ], 80 | "subInstructions": [] 81 | } 82 | ], 83 | "events": [] 84 | } 85 | ], 86 | "parameters": [], 87 | "objectGroups": [] 88 | } 89 | ], 90 | "eventsBasedBehaviors": [] 91 | } -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/empty.zip: -------------------------------------------------------------------------------- 1 | PK������������������ -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/invalid-zip.zip: -------------------------------------------------------------------------------- 1 | We're no strangers to love 2 | You know the rules and so do I (do I) 3 | A full commitment's what I'm thinking of 4 | You wouldn't get this from any other guy 5 | I just wanna tell you how I'm feeling 6 | Gotta make you understand 7 | Never gonna give you up 8 | Never gonna let you down 9 | Never gonna run around and desert you 10 | Never gonna make you cry 11 | Never gonna say goodbye 12 | Never gonna tell a lie and hurt you 13 | We've known each other for so long 14 | Your heart's been aching, but you're too shy to say it (say it) 15 | Inside, we both know what's been going on (going on) 16 | We know the game and we're gonna play it 17 | And if you ask me how I'm feeling 18 | Don't tell me you're too blind to see 19 | Never gonna give you up 20 | Never gonna let you down 21 | Never gonna run around and desert you 22 | Never gonna make you cry 23 | Never gonna say goodbye 24 | Never gonna tell a lie and hurt you 25 | Never gonna give you up 26 | Never gonna let you down 27 | Never gonna run around and desert you 28 | Never gonna make you cry 29 | Never gonna say goodbye 30 | Never gonna tell a lie and hurt you 31 | We've known each other for so long 32 | Your heart's been aching, but you're too shy to say it (to say it) 33 | Inside, we both know what's been going on (going on) 34 | We know the game and we're gonna play it 35 | I just wanna tell you how I'm feeling 36 | Gotta make you understand 37 | Never gonna give you up 38 | Never gonna let you down 39 | Never gonna run around and desert you 40 | Never gonna make you cry 41 | Never gonna say goodbye 42 | Never gonna tell a lie and hurt you 43 | Never gonna give you up 44 | Never gonna let you down 45 | Never gonna run around and desert you 46 | Never gonna make you cry 47 | Never gonna say goodbye 48 | Never gonna tell a lie and hurt you 49 | Never gonna give you up 50 | Never gonna let you down 51 | Never gonna run around and desert you 52 | Never gonna make you cry 53 | Never gonna say goodbye 54 | Never gonna tell a lie and hurt you -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/not-a-json.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/__tests__/auto-pr/test-zips/not-a-json.zip -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/path-shenanigans.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/__tests__/auto-pr/test-zips/path-shenanigans.zip -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/too-many-extensions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/__tests__/auto-pr/test-zips/too-many-extensions.zip -------------------------------------------------------------------------------- /__tests__/auto-pr/test-zips/valid-extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/__tests__/auto-pr/test-zips/valid-extension.zip -------------------------------------------------------------------------------- /__tests__/post-build/extensions.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require('fs').promises; 3 | const path = require('path'); 4 | 5 | /** @typedef {import('../../scripts/types').ExtensionsDatabase} ExtensionsDatabase */ 6 | 7 | const distExtensionsPath = path.join(__dirname, '../../dist/extensions'); 8 | 9 | /** @return {Promise<ExtensionsDatabase>} */ 10 | const getExtensionsDatabase = async () => { 11 | const extensionsDatabase = await fs.readFile( 12 | path.join( 13 | __dirname, 14 | '../../dist/extensions-database/extensions-database.json' 15 | ) 16 | ); 17 | const parsedExtensionsDatabase = JSON.parse(extensionsDatabase.toString()); 18 | return parsedExtensionsDatabase; 19 | }; 20 | 21 | describe('extensions database post check', () => { 22 | test('extensions-database.json', async () => { 23 | const extensionsDatabase = await getExtensionsDatabase(); 24 | 25 | expect(extensionsDatabase.views.default.firstExtensionIds).toContain( 26 | 'Health' 27 | ); 28 | 29 | // Check that the headers seem correct 30 | expect(extensionsDatabase.extensionShortHeaders.length).toBeGreaterThan(70); 31 | 32 | const draggableSliderControlExtensionShortHeader = 33 | extensionsDatabase.extensionShortHeaders.find( 34 | ({ name }) => name === 'DraggableSliderControl' 35 | ); 36 | const fireBulletExtensionShortHeader = 37 | extensionsDatabase.extensionShortHeaders.find( 38 | ({ name }) => name === 'FireBullet' 39 | ); 40 | const followObjectsWithCameraExtensionShortHeader = 41 | extensionsDatabase.extensionShortHeaders.find( 42 | ({ name }) => name === 'FollowObjectsWithCamera' 43 | ); 44 | const iframeExtensionShortHeader = 45 | extensionsDatabase.extensionShortHeaders.find( 46 | ({ name }) => name === 'Iframe' 47 | ); 48 | 49 | if (!draggableSliderControlExtensionShortHeader) 50 | throw new Error('DraggableSliderControl extension not found.'); 51 | if (!fireBulletExtensionShortHeader) 52 | throw new Error('FireBullet extension not found.'); 53 | if (!followObjectsWithCameraExtensionShortHeader) 54 | throw new Error('FollowObjectsWithCamera extension not found.'); 55 | if (!iframeExtensionShortHeader) 56 | throw new Error('Iframe extension not found.'); 57 | 58 | // Check the content of some extension headers 59 | expect(fireBulletExtensionShortHeader.tier).toBe('reviewed'); 60 | expect(iframeExtensionShortHeader.tier).toBe('reviewed'); 61 | }); 62 | test('extensions', async () => { 63 | // Check that extensions are present. 64 | await expect( 65 | fs.stat(path.join(distExtensionsPath, '/FireBullet.json')) 66 | ).resolves.toBeDefined(); 67 | await expect( 68 | fs.stat(path.join(distExtensionsPath, '/FireBullet-header.json')) 69 | ).resolves.toBeDefined(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | 'project_id_env': 'CROWDIN_PROJECT_ID' 2 | 'api_token_env': 'CROWDIN_PERSONAL_TOKEN' 3 | 'base_path': '.' 4 | 'base_url': 'https://api.crowdin.com' 5 | 6 | # Flatten files in Crowdin 7 | 'preserve_hierarchy': false 8 | 9 | # "Source" files, which are .POT files extracted from the source code. 10 | # The built files are .PO files that are the compiled by `compile-translations` 11 | # script in `scripts`. 12 | files: 13 | [ 14 | { 15 | 'source': '.translations/en/reviewed-extensions-messages.pot', 16 | 'translation': '.translations/%locale_with_underscore%/reviewed-extensions-messages.po', 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /extensions/community/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GDevelopApp/GDevelop-extensions/2bf98ad8500a4013e89e334af00147a485034c55/extensions/community/.gitkeep -------------------------------------------------------------------------------- /extensions/community/Choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ulises Freitas <ulises.freitas@gmail.com>", 3 | "category": "General", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Choose a random value (deprecated)", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWRpY2UtbXVsdGlwbGUiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTkuNzgsM0gxMS4yMkMxMC41NSwzIDEwLDMuNTUgMTAsNC4yMlY4SDE2VjE0SDE5Ljc4QzIwLjQ1LDE0IDIxLDEzLjQ1IDIxLDEyLjc4VjQuMjJDMjEsMy41NSAyMC40NSwzIDE5Ljc4LDNNMTIuNDQsNi42N0MxMS43Niw2LjY3IDExLjIxLDYuMTIgMTEuMjEsNS40NEMxMS4yMSw0Ljc2IDExLjc2LDQuMjEgMTIuNDQsNC4yMUExLjIzLDEuMjMgMCAwLDEgMTMuNjcsNS40NEMxMy42Nyw2LjEyIDEzLjEyLDYuNjcgMTIuNDQsNi42N00xOC41NiwxMi43OEMxNy44OCwxMi43OSAxNy4zMywxMi4yNCAxNy4zMiwxMS41NkMxNy4zMSwxMC44OCAxNy44NiwxMC4zMyAxOC41NCwxMC4zMkMxOS4yMiwxMC4zMSAxOS43NywxMC44NiAxOS43OCwxMS41NkMxOS43NywxMi4yMyAxOS4yMywxMi43NyAxOC41NiwxMi43OE0xOC41Niw2LjY3QzE3Ljg4LDYuNjggMTcuMzMsNi4xMyAxNy4zMiw1LjQ1QzE3LjMxLDQuNzcgMTcuODYsNC4yMiAxOC41NCw0LjIxQzE5LjIyLDQuMiAxOS43Nyw0Ljc1IDE5Ljc4LDUuNDRDMTkuNzgsNi4xMiAxOS4yNCw2LjY2IDE4LjU2LDYuNjdNNC4yMiwxMEgxMi43OEExLjIyLDEuMjIgMCAwLDEgMTQsMTEuMjJWMTkuNzhDMTQsMjAuNDUgMTMuNDUsMjEgMTIuNzgsMjFINC4yMkMzLjU1LDIxIDMsMjAuNDUgMywxOS43OFYxMS4yMkMzLDEwLjU1IDMuNTUsMTAgNC4yMiwxME04LjUsMTQuMjhDNy44MywxNC4yOCA3LjI4LDE0LjgzIDcuMjgsMTUuNUM3LjI4LDE2LjE3IDcuODMsMTYuNzIgOC41LDE2LjcyQzkuMTcsMTYuNzIgOS43MiwxNi4xNyA5LjcyLDE1LjVBMS4yMiwxLjIyIDAgMCwwIDguNSwxNC4yOE01LjQ0LDExLjIyQzQuNzcsMTEuMjIgNC4yMiwxMS43NyA0LjIyLDEyLjQ0QTEuMjIsMS4yMiAwIDAsMCA1LjQ0LDEzLjY2QzYuMTEsMTMuNjYgNi42NiwxMy4xMSA2LjY2LDEyLjQ0VjEyLjQ0QzYuNjYsMTEuNzcgNi4xMSwxMS4yMiA1LjQ0LDExLjIyTTExLjU1LDE3LjMzQzEwLjg4LDE3LjMzIDEwLjMzLDE3Ljg4IDEwLjMzLDE4LjU1QzEwLjMzLDE5LjIyIDEwLjg4LDE5Ljc3IDExLjU1LDE5Ljc3QTEuMjIsMS4yMiAwIDAsMCAxMi43NywxOC41NUgxMi43N0MxMi43NywxNy44OCAxMi4yMywxNy4zNCAxMS41NiwxNy4zM0gxMS41NVoiIC8+PC9zdmc+", 9 | "name": "Choose", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/dice-multiple.svg", 11 | "shortDescription": "Choose a random value in a list of strings or numbers.", 12 | "version": "1.0.0", 13 | "description": [ 14 | "In an expression, use `Choose::RandomString` or `Choose::RandomNumber` and set the values you want to randomize separated by commas.", 15 | "", 16 | "This extension is deprecated. Use the [Array Tools extension](https://wiki.gdevelop.io/gdevelop5/extensions/array-tools) to get a random value from an array variable instead." 17 | ], 18 | "origin": { 19 | "identifier": "Choose", 20 | "name": "gdevelop-extension-store" 21 | }, 22 | "tags": [ 23 | "choose", 24 | "random" 25 | ], 26 | "authorIds": [ 27 | "ZShmW1xkW7WWl9AkB78VITJMiTw1" 28 | ], 29 | "dependencies": [], 30 | "globalVariables": [], 31 | "sceneVariables": [], 32 | "eventsFunctions": [ 33 | { 34 | "description": "Choose randomly between comma-separated strings.", 35 | "fullName": "Choose randomly between comma-separated strings", 36 | "functionType": "StringExpression", 37 | "name": "RandomString", 38 | "sentence": "Choose between these values: _PARAM1_", 39 | "events": [ 40 | { 41 | "type": "BuiltinCommonInstructions::JsCode", 42 | "inlineCode": [ 43 | "/** @type {string[]} */", 44 | "const choices = eventsFunctionContext.getArgument(\"ChoicesString\").split(',');", 45 | "eventsFunctionContext.returnValue = choices[Math.floor(Math.random() * choices.length)];", 46 | "" 47 | ], 48 | "parameterObjects": "", 49 | "useStrict": true, 50 | "eventsSheetExpanded": true 51 | } 52 | ], 53 | "expressionType": { 54 | "type": "string" 55 | }, 56 | "parameters": [ 57 | { 58 | "description": "The string containing all options to choose randomly from, separated by commas", 59 | "longDescription": "Example: \"option1,option2,option3\"", 60 | "name": "ChoicesString", 61 | "type": "string" 62 | } 63 | ], 64 | "objectGroups": [] 65 | }, 66 | { 67 | "description": "Choose a number randomly between comma-separated numbers.", 68 | "fullName": "Choose a number randomly between comma-separated numbers", 69 | "functionType": "Expression", 70 | "name": "RandomNumber", 71 | "sentence": "Choose between these values: _PARAM1_", 72 | "events": [ 73 | { 74 | "type": "BuiltinCommonInstructions::JsCode", 75 | "inlineCode": [ 76 | "/** @type {string[]} */", 77 | "const choices = eventsFunctionContext.getArgument(\"NumbersChoiceString\").split(',');", 78 | "eventsFunctionContext.returnValue = parseFloat(choices[Math.floor(Math.random() * choices.length)]);", 79 | "" 80 | ], 81 | "parameterObjects": "", 82 | "useStrict": true, 83 | "eventsSheetExpanded": true 84 | } 85 | ], 86 | "expressionType": { 87 | "type": "expression" 88 | }, 89 | "parameters": [ 90 | { 91 | "description": "The string containing all numbers to choose randomly from, separated by commas", 92 | "longDescription": "Example: \"10,20,30\"", 93 | "name": "NumbersChoiceString", 94 | "type": "string" 95 | } 96 | ], 97 | "objectGroups": [] 98 | } 99 | ], 100 | "eventsBasedBehaviors": [], 101 | "eventsBasedObjects": [] 102 | } -------------------------------------------------------------------------------- /extensions/community/CryptoApi.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "Advanced", 4 | "description": "This uses the Crypto API to create a random number see help for more details. \n\nYou might ask how this differs from the built-in random functions in GDevelop like RandomInRange. In basic terms, it provides a more random number than Math.random() which is what the built-in functions use.\nThat randomness does come at a cost of performance so be aware of your usage of this. \n\n**This has nothing to do with crypto currency**", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Crypto Api", 8 | "helpPath": "https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXNoaWVsZC1rZXkiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIsOEExLDEgMCAwLDEgMTMsOUExLDEgMCAwLDEgMTIsMTBBMSwxIDAgMCwxIDExLDlBMSwxIDAgMCwxIDEyLDhNMjEsMTFDMjEsMTYuNTUgMTcuMTYsMjEuNzQgMTIsMjNDNi44NCwyMS43NCAzLDE2LjU1IDMsMTFWNUwxMiwxTDIxLDVWMTFNMTIsNkEzLDMgMCAwLDAgOSw5QzksMTAuMzEgOS44MywxMS40MiAxMSwxMS44M1YxOEgxM1YxNkgxNVYxNEgxM1YxMS44M0MxNC4xNywxMS40MiAxNSwxMC4zMSAxNSw5QTMsMyAwIDAsMCAxMiw2WiIgLz48L3N2Zz4=", 10 | "name": "CryptoApi", 11 | "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/fac7ecb74ef7da92ab59c3e431fb9587c105c2889a41cfac489135c0eb4643d1_shield-key.svg", 12 | "shortDescription": "Random number generator for integers and floats using the Crypto API.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "random", 16 | "crypto", 17 | "math" 18 | ], 19 | "authorIds": [], 20 | "dependencies": [], 21 | "eventsFunctions": [ 22 | { 23 | "description": "Uses the Crypto API to create a longer random number in an integer range.", 24 | "fullName": "Random In Range", 25 | "functionType": "Expression", 26 | "group": "", 27 | "name": "RandomInRange", 28 | "private": false, 29 | "sentence": "", 30 | "events": [ 31 | { 32 | "type": "BuiltinCommonInstructions::JsCode", 33 | "inlineCode": "const minParam = eventsFunctionContext.getArgument(\"Minimum\");\nconst maxParam = eventsFunctionContext.getArgument(\"Maximum\");\n\n/**\n * inclusive means in a range of 1-10 it includes 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 \n * exclusive would be 1, 2, 3, 4, 5, 6, 7, 8, 9 \n * @min: Min value to be returned (inclusive)\n * @max: Max value to be returned (inclusive)\n */\nconst getRandomNum = (min, max) => {\n const randomArraryBuffer = new Uint32Array(1);\n crypto.getRandomValues(randomArraryBuffer);\n const randomNum = randomArraryBuffer[0] / (0xffffffff + 1);\n\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(randomNum * (max - min + 1)) + min;\n}\n\neventsFunctionContext.returnValue = getRandomNum(minParam, maxParam);", 34 | "parameterObjects": "", 35 | "useStrict": true, 36 | "eventsSheetExpanded": true 37 | } 38 | ], 39 | "parameters": [ 40 | { 41 | "codeOnly": false, 42 | "defaultValue": "", 43 | "description": "Minimum Value ", 44 | "longDescription": "", 45 | "name": "Minimum", 46 | "optional": false, 47 | "supplementaryInformation": "", 48 | "type": "expression" 49 | }, 50 | { 51 | "codeOnly": false, 52 | "defaultValue": "", 53 | "description": "Maximum Value ", 54 | "longDescription": "", 55 | "name": "Maximum", 56 | "optional": false, 57 | "supplementaryInformation": "", 58 | "type": "expression" 59 | } 60 | ], 61 | "objectGroups": [] 62 | }, 63 | { 64 | "description": "Uses the Crypto API to create a longer random number in an float range.", 65 | "fullName": "Random Float In Range", 66 | "functionType": "Expression", 67 | "group": "", 68 | "name": "RandomFloatInRange", 69 | "private": false, 70 | "sentence": "", 71 | "events": [ 72 | { 73 | "type": "BuiltinCommonInstructions::JsCode", 74 | "inlineCode": "const minParam = eventsFunctionContext.getArgument(\"Minimum\");\nconst maxParam = eventsFunctionContext.getArgument(\"Maximum\");\n\n/**\n * @min: Min value to be returned (inclusive)\n * @max: Max value to be returned (inclusive)\n */\nconst getRandomNumFloat = (min, max) => {\n\n const randomArraryBuffer = new Uint32Array(1);\n crypto.getRandomValues(randomArraryBuffer);\n const randomNum = randomArraryBuffer[0] / (0xffffffff + 1);\n\n min = Math.ceil(min);\n max = Math.floor(max);\n return randomNum * (max - min + 1) + min;\n}\n\neventsFunctionContext.returnValue = getRandomNumFloat(minParam, maxParam) || 0;", 75 | "parameterObjects": "", 76 | "useStrict": true, 77 | "eventsSheetExpanded": true 78 | } 79 | ], 80 | "parameters": [ 81 | { 82 | "codeOnly": false, 83 | "defaultValue": "", 84 | "description": "Minimum Value ", 85 | "longDescription": "", 86 | "name": "Minimum", 87 | "optional": false, 88 | "supplementaryInformation": "", 89 | "type": "expression" 90 | }, 91 | { 92 | "codeOnly": false, 93 | "defaultValue": "", 94 | "description": "Maximum Value ", 95 | "longDescription": "", 96 | "name": "Maximum", 97 | "optional": false, 98 | "supplementaryInformation": "", 99 | "type": "expression" 100 | } 101 | ], 102 | "objectGroups": [] 103 | } 104 | ], 105 | "eventsBasedBehaviors": [] 106 | } -------------------------------------------------------------------------------- /extensions/community/Geolocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "category": "Device", 4 | "description": "Adds an action to locate the player using the built-in GPS, or other techniques if unavailable. \nYou can also track whether or not you've gotten the permission to use the GPS sensor, and track realtime player movement.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "GPS", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWNyb3NzaGFpcnMtZ3BzIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyLDhBNCw0IDAgMCwxIDE2LDEyQTQsNCAwIDAsMSAxMiwxNkE0LDQgMCAwLDEgOCwxMkE0LDQgMCAwLDEgMTIsOE0zLjA1LDEzSDFWMTFIMy4wNUMzLjUsNi44MyA2LjgzLDMuNSAxMSwzLjA1VjFIMTNWMy4wNUMxNy4xNywzLjUgMjAuNSw2LjgzIDIwLjk1LDExSDIzVjEzSDIwLjk1QzIwLjUsMTcuMTcgMTcuMTcsMjAuNSAxMywyMC45NVYyM0gxMVYyMC45NUM2LjgzLDIwLjUgMy41LDE3LjE3IDMuMDUsMTNNMTIsNUE3LDcgMCAwLDAgNSwxMkE3LDcgMCAwLDAgMTIsMTlBNyw3IDAgMCwwIDE5LDEyQTcsNyAwIDAsMCAxMiw1WiIgLz48L3N2Zz4=", 10 | "name": "Geolocation", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/crosshairs-gps.svg", 12 | "shortDescription": "Adds a way to locate the player.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "geolocation", 16 | "gps", 17 | "position", 18 | "location", 19 | "locate", 20 | "where", 21 | "place" 22 | ], 23 | "authorIds": [ 24 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 25 | ], 26 | "dependencies": [ 27 | { 28 | "exportName": "cordova-plugin-geolocation", 29 | "name": "Geolocation cordova support", 30 | "type": "cordova", 31 | "version": "https://github.com/apache/cordova-plugin-geolocation.git" 32 | } 33 | ], 34 | "eventsFunctions": [ 35 | { 36 | "description": "", 37 | "fullName": "", 38 | "functionType": "Action", 39 | "group": "", 40 | "name": "onFirstSceneLoaded", 41 | "private": false, 42 | "sentence": "", 43 | "events": [ 44 | { 45 | "type": "BuiltinCommonInstructions::JsCode", 46 | "inlineCode": "gdjs.evtTools.geolocation = {\n permission: { state: navigator.permissions ? \"loading\" : \"unknown\" }\n};\n\n// Prefetch the permission and set up an event handler to keep it up to date\nif (navigator.permissions) navigator.permissions.query({ name: 'geolocation' })\n .then((permission) => {\n gdjs.evtTools.geolocation.permission = permission;\n });\n", 47 | "parameterObjects": "", 48 | "useStrict": true, 49 | "eventsSheetExpanded": false 50 | } 51 | ], 52 | "parameters": [], 53 | "objectGroups": [] 54 | }, 55 | { 56 | "description": "Returns the status of the geolocation permission.", 57 | "fullName": "Permission status", 58 | "functionType": "StringExpression", 59 | "group": "", 60 | "name": "PermissionStatus", 61 | "private": false, 62 | "sentence": "", 63 | "events": [ 64 | { 65 | "type": "BuiltinCommonInstructions::JsCode", 66 | "inlineCode": "eventsFunctionContext.returnValue = gdjs.evtTools.geolocation.permission.state;\n", 67 | "parameterObjects": "", 68 | "useStrict": true, 69 | "eventsSheetExpanded": false 70 | } 71 | ], 72 | "parameters": [], 73 | "objectGroups": [] 74 | }, 75 | { 76 | "description": "Detects when the player's device is moved and get its new location. If the permission status is pending, it will ask for user permission. Every time the location is updated, stores it in the callback variable.", 77 | "fullName": "Watch the player", 78 | "functionType": "Action", 79 | "group": "", 80 | "name": "WatchPlayer", 81 | "private": false, 82 | "sentence": "Watch player movements and store their position in scene variable _PARAM1_", 83 | "events": [ 84 | { 85 | "type": "BuiltinCommonInstructions::JsCode", 86 | "inlineCode": "this.logger = (this.logger || new gdjs.Logger(\"Geolocation extension\"))\n\nif (navigator && navigator.geolocation) navigator\n .geolocation\n .watchPosition(\n location => {\n const variable = eventsFunctionContext.getArgument(\"callback\");\n for (const child of Object.keys(location.coords.__proto__)) {\n variable.getChild(child).setNumber(location.coords[child]);\n }\n },\n (error) => this.logger.error(`Couldn't locate player: ` + error.message),\n { enableHighAccuracy: true }\n );\n", 87 | "parameterObjects": "", 88 | "useStrict": true, 89 | "eventsSheetExpanded": false 90 | } 91 | ], 92 | "parameters": [ 93 | { 94 | "codeOnly": false, 95 | "defaultValue": "", 96 | "description": "Callback variable", 97 | "longDescription": "", 98 | "name": "callback", 99 | "optional": false, 100 | "supplementaryInformation": "", 101 | "type": "scenevar" 102 | } 103 | ], 104 | "objectGroups": [] 105 | }, 106 | { 107 | "description": "Locates the player. If the permission status is pending, it will ask for user permission. Once the location is fetched, stores it in the callback variable.", 108 | "fullName": "Locate the player", 109 | "functionType": "Action", 110 | "group": "", 111 | "name": "LocatePlayer", 112 | "private": false, 113 | "sentence": "Locate the player and store the result in scene variable _PARAM1_", 114 | "events": [ 115 | { 116 | "type": "BuiltinCommonInstructions::JsCode", 117 | "inlineCode": "this.logger = (this.logger || new gdjs.Logger(\"Geolocation extension\"))\n\nif (navigator && navigator.geolocation) navigator\n .geolocation\n .getCurrentPosition(\n location => {\n const variable = eventsFunctionContext.getArgument(\"callback\");\n for (const child in location.coords) {\n variable.getChild(child).setNumber(location.coords[child]);\n }\n },\n (error) => this.logger.error(`Couldn't locate player: ` + error.message),\n { enableHighAccuracy: true }\n );\n", 118 | "parameterObjects": "", 119 | "useStrict": true, 120 | "eventsSheetExpanded": false 121 | } 122 | ], 123 | "parameters": [ 124 | { 125 | "codeOnly": false, 126 | "defaultValue": "", 127 | "description": "Callback variable", 128 | "longDescription": "", 129 | "name": "callback", 130 | "optional": false, 131 | "supplementaryInformation": "", 132 | "type": "scenevar" 133 | } 134 | ], 135 | "objectGroups": [] 136 | } 137 | ], 138 | "eventsBasedBehaviors": [] 139 | } -------------------------------------------------------------------------------- /extensions/community/JSONResourceLoader.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "General", 4 | "description": "Loads a (static) JSON resource from your project files into a global, scene, or object variable.\n\nNEVER use this to load your game.json into a variable - this would increase your game size by a lot and make your whole project file available for anyone to open!", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "JSON Resource Loading", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZpbGUtY29kZS1vdXRsaW5lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE0IDJINkM0Ljg5IDIgNCAyLjkgNCA0VjIwQzQgMjEuMTEgNC44OSAyMiA2IDIySDE4QzE5LjExIDIyIDIwIDIxLjExIDIwIDIwVjhMMTQgMk0xOCAyMEg2VjRIMTNWOUgxOFYyME05LjU0IDE1LjY1TDExLjYzIDE3Ljc0TDEwLjM1IDE5TDcgMTUuNjVMMTAuMzUgMTIuM0wxMS42MyAxMy41Nkw5LjU0IDE1LjY1TTE3IDE1LjY1TDEzLjY1IDE5TDEyLjM4IDE3Ljc0TDE0LjQ3IDE1LjY1TDEyLjM4IDEzLjU2TDEzLjY1IDEyLjNMMTcgMTUuNjVaIiAvPjwvc3ZnPg==", 10 | "name": "JSONResourceLoader", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/file-code-outline.svg", 12 | "shortDescription": "Loads a JSON resource into a variable.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "json", 16 | "resource", 17 | "load", 18 | "open", 19 | "file", 20 | "data", 21 | "static", 22 | "file" 23 | ], 24 | "authorIds": [ 25 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 26 | ], 27 | "dependencies": [], 28 | "eventsFunctions": [ 29 | { 30 | "description": "Loads a JSON resource into a scene structure variable.", 31 | "fullName": "Load a JSON resource in a scene variable", 32 | "functionType": "Action", 33 | "group": "", 34 | "name": "LoadJSONToScene", 35 | "private": false, 36 | "sentence": "Load _PARAM1_ into scene variable _PARAM2_", 37 | "events": [ 38 | { 39 | "type": "BuiltinCommonInstructions::JsCode", 40 | "inlineCode": "eventsFunctionContext\n .getArgument(\"Variable\")\n .fromJSObject(\n runtimeScene\n .getGame()\n .getJsonManager()\n .getLoadedJson(eventsFunctionContext.getArgument(\"Resource\"))\n );\n", 41 | "parameterObjects": "", 42 | "useStrict": true, 43 | "eventsSheetExpanded": false 44 | } 45 | ], 46 | "parameters": [ 47 | { 48 | "codeOnly": false, 49 | "defaultValue": "", 50 | "description": "The resource to load the JSON from", 51 | "longDescription": "", 52 | "name": "Resource", 53 | "optional": false, 54 | "supplementaryInformation": "", 55 | "type": "jsonResource" 56 | }, 57 | { 58 | "codeOnly": false, 59 | "defaultValue": "", 60 | "description": "The scene variable to load the JSON to", 61 | "longDescription": "", 62 | "name": "Variable", 63 | "optional": false, 64 | "supplementaryInformation": "", 65 | "type": "scenevar" 66 | } 67 | ], 68 | "objectGroups": [] 69 | }, 70 | { 71 | "description": "Loads a JSON resource into a global structure variable.", 72 | "fullName": "Load a JSON resource in a global variable", 73 | "functionType": "Action", 74 | "group": "", 75 | "name": "LoadJSONToGlobal", 76 | "private": false, 77 | "sentence": "Load _PARAM1_ into global variable _PARAM2_", 78 | "events": [ 79 | { 80 | "type": "BuiltinCommonInstructions::JsCode", 81 | "inlineCode": "eventsFunctionContext\n .getArgument(\"Variable\")\n .fromJSObject(\n runtimeScene\n .getGame()\n .getJsonManager()\n .getLoadedJson(eventsFunctionContext.getArgument(\"Resource\"))\n );\n", 82 | "parameterObjects": "", 83 | "useStrict": true, 84 | "eventsSheetExpanded": false 85 | } 86 | ], 87 | "parameters": [ 88 | { 89 | "codeOnly": false, 90 | "defaultValue": "", 91 | "description": "The resource to load the JSON from", 92 | "longDescription": "", 93 | "name": "Resource", 94 | "optional": false, 95 | "supplementaryInformation": "", 96 | "type": "jsonResource" 97 | }, 98 | { 99 | "codeOnly": false, 100 | "defaultValue": "", 101 | "description": "The global variable to load the JSON to", 102 | "longDescription": "", 103 | "name": "Variable", 104 | "optional": false, 105 | "supplementaryInformation": "", 106 | "type": "globalvar" 107 | } 108 | ], 109 | "objectGroups": [] 110 | }, 111 | { 112 | "description": "Loads a JSON resource into an object structure variable.", 113 | "fullName": "Load a JSON resource in an object variable", 114 | "functionType": "Action", 115 | "group": "", 116 | "name": "LoadJSONToObject", 117 | "private": false, 118 | "sentence": "Load _PARAM1_ into object variable _PARAM3_ of object _PARAM2_", 119 | "events": [ 120 | { 121 | "type": "BuiltinCommonInstructions::JsCode", 122 | "inlineCode": "eventsFunctionContext\n .getArgument(\"Variable\")\n .fromJSObject(\n runtimeScene\n .getGame()\n .getJsonManager()\n .getLoadedJson(eventsFunctionContext.getArgument(\"Resource\"))\n );\n", 123 | "parameterObjects": "", 124 | "useStrict": true, 125 | "eventsSheetExpanded": false 126 | } 127 | ], 128 | "parameters": [ 129 | { 130 | "codeOnly": false, 131 | "defaultValue": "", 132 | "description": "The resource to load the JSON from", 133 | "longDescription": "", 134 | "name": "Resource", 135 | "optional": false, 136 | "supplementaryInformation": "", 137 | "type": "jsonResource" 138 | }, 139 | { 140 | "codeOnly": false, 141 | "defaultValue": "", 142 | "description": "The object where to find the variable", 143 | "longDescription": "", 144 | "name": "Object", 145 | "optional": false, 146 | "supplementaryInformation": "", 147 | "type": "objectList" 148 | }, 149 | { 150 | "codeOnly": false, 151 | "defaultValue": "", 152 | "description": "The object variable to load the JSON to", 153 | "longDescription": "", 154 | "name": "Variable", 155 | "optional": false, 156 | "supplementaryInformation": "", 157 | "type": "objectvar" 158 | } 159 | ], 160 | "objectGroups": [] 161 | } 162 | ], 163 | "eventsBasedBehaviors": [], 164 | "eventsBasedObjects": [] 165 | } -------------------------------------------------------------------------------- /extensions/community/LoadImageFromURL.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "General", 4 | "description": "With this extension, you can load images from any URL (including a DataURL) into a sprite or image resource.\nLoading it into a sprite will load the URL into a sprite, replacing the image it displays until the sprite's displayed image is changed, e.g. by going to the next frame of an animation or switching animations.\nLoading it into a resource will discard the old image that a resource was using and replace it with the image loaded from a URL. Any sprite that is displaying this resource as a part of their animation will start showing the new image instead of the old one. The old image of the resource will not be accessible anymore unless you restart the game or reload the original image from a URL into it.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Load images from a URL", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZpbGUtZG93bmxvYWQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTQsMkg2QzQuODksMiA0LDIuODkgNCw0VjIwQzQsMjEuMTEgNC44OSwyMiA2LDIySDE4QzE5LjExLDIyIDIwLDIxLjExIDIwLDIwVjhMMTQsMk0xMiwxOUw4LDE1SDEwLjVWMTJIMTMuNVYxNUgxNkwxMiwxOU0xMyw5VjMuNUwxOC41LDlIMTNaIiAvPjwvc3ZnPg==", 10 | "name": "LoadImageFromURL", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/file-download.svg", 12 | "shortDescription": "Adds multiple actions to load images from a URL into the game.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "resource", 16 | "art", 17 | "image", 18 | "url", 19 | "base64", 20 | "data", 21 | "url", 22 | "load", 23 | "json", 24 | "file", 25 | "download", 26 | "get", 27 | "request" 28 | ], 29 | "authorIds": [ 30 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 31 | ], 32 | "dependencies": [], 33 | "eventsFunctions": [ 34 | { 35 | "description": "Replaces the image contained by a sprite by a new one, from a URL. This will only affect the sprite in question and only until the image in it is changed through its animation or another action, unless you also modify the resource.", 36 | "fullName": "Load URL into a sprite", 37 | "functionType": "Action", 38 | "group": "", 39 | "name": "LoadURLIntoSprite", 40 | "private": false, 41 | "sentence": "Load URL _PARAM1_ into sprite _PARAM2_ (and into the corresponding resource: _PARAM3_)", 42 | "events": [ 43 | { 44 | "type": "BuiltinCommonInstructions::JsCode", 45 | "inlineCode": "if (eventsFunctionContext.getArgument(\"ChangeResource\")) {\n const texture = PIXI.BaseTexture.from(eventsFunctionContext.getArgument(\"URL\"));\n for (const obj of objects) obj.getRendererObject().texture.baseTexture = texture;\n} else {\n const texture = PIXI.Texture.from(eventsFunctionContext.getArgument(\"URL\"));\n for (const obj of objects) obj.getRendererObject().texture = texture;\n}\n", 46 | "parameterObjects": "Object", 47 | "useStrict": true, 48 | "eventsSheetExpanded": false 49 | } 50 | ], 51 | "parameters": [ 52 | { 53 | "codeOnly": false, 54 | "defaultValue": "", 55 | "description": "The URL to load the new image for the sprite from", 56 | "longDescription": "", 57 | "name": "URL", 58 | "optional": false, 59 | "supplementaryInformation": "", 60 | "type": "string" 61 | }, 62 | { 63 | "codeOnly": false, 64 | "defaultValue": "", 65 | "description": "The object to modify", 66 | "longDescription": "", 67 | "name": "Object", 68 | "optional": false, 69 | "supplementaryInformation": "Sprite", 70 | "type": "objectList" 71 | }, 72 | { 73 | "codeOnly": false, 74 | "defaultValue": "", 75 | "description": "Modify the resource?", 76 | "longDescription": "If yes, modifies the image contained in the resource of the sprite's current frame instead of just the sprite's displayed image. This makes the changes affect all other sprites that also display this resource, and allows the change to persist after changing animations or the current frame.", 77 | "name": "ChangeResource", 78 | "optional": false, 79 | "supplementaryInformation": "", 80 | "type": "yesorno" 81 | } 82 | ], 83 | "objectGroups": [] 84 | }, 85 | { 86 | "description": "Replaces the image contained by a resource by a new one, from a URL. This will update all sprites displaying the resource.", 87 | "fullName": "Load URL into an image resource", 88 | "functionType": "Action", 89 | "group": "", 90 | "name": "LoadURLIntoImageResource", 91 | "private": false, 92 | "sentence": "Load URL _PARAM1_ into resource _PARAM2_", 93 | "events": [ 94 | { 95 | "type": "BuiltinCommonInstructions::JsCode", 96 | "inlineCode": "runtimeScene\n .getGame()\n .getImageManager()\n .getPIXITexture(eventsFunctionContext.getArgument(\"Resource\"))\n .baseTexture = PIXI.BaseTexture.from(eventsFunctionContext.getArgument(\"URL\"));\n", 97 | "parameterObjects": "", 98 | "useStrict": true, 99 | "eventsSheetExpanded": false 100 | } 101 | ], 102 | "parameters": [ 103 | { 104 | "codeOnly": false, 105 | "defaultValue": "", 106 | "description": "The URL to load the new image for the resource from", 107 | "longDescription": "", 108 | "name": "URL", 109 | "optional": false, 110 | "supplementaryInformation": "", 111 | "type": "string" 112 | }, 113 | { 114 | "codeOnly": false, 115 | "defaultValue": "", 116 | "description": "The resource to modify", 117 | "longDescription": "", 118 | "name": "Resource", 119 | "optional": false, 120 | "supplementaryInformation": "", 121 | "type": "imageResource" 122 | } 123 | ], 124 | "objectGroups": [] 125 | } 126 | ], 127 | "eventsBasedBehaviors": [], 128 | "eventsBasedObjects": [] 129 | } -------------------------------------------------------------------------------- /extensions/community/OllamaAI.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "Third-party", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "OllamaAI", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWNoYXQtcHJvY2Vzc2luZy1vdXRsaW5lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDNDNi41IDMgMiA2LjU4IDIgMTFDMi4wNSAxMy4xNSAzLjA2IDE1LjE3IDQuNzUgMTYuNUM0Ljc1IDE3LjEgNC4zMyAxOC42NyAyIDIxQzQuMzcgMjAuODkgNi42NCAyMCA4LjQ3IDE4LjVDOS42MSAxOC44MyAxMC44MSAxOSAxMiAxOUMxNy41IDE5IDIyIDE1LjQyIDIyIDExUzE3LjUgMyAxMiAzTTEyIDE3QzcuNTggMTcgNCAxNC4zMSA0IDExUzcuNTggNSAxMiA1IDIwIDcuNjkgMjAgMTEgMTYuNDIgMTcgMTIgMTdNMTcgMTJWMTBIMTVWMTJIMTdNMTMgMTJWMTBIMTFWMTJIMTNNOSAxMlYxMEg3VjEySDlaIiAvPjwvc3ZnPg==", 9 | "name": "OllamaAI", 10 | "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/17afc029e0ccfc314f5b7f5f53632ca24b3e6082d0cc87c0777dcfa79fd56e0b_chat-processing-outline.svg", 11 | "shortDescription": "This extension adds the functionality to your project to easily send requests to the \"Ollama\" AI, and get responses from it.", 12 | "version": "1.0.0", 13 | "description": [ 14 | "Create a simple action to send the following data to a Ollama AI server:", 15 | "", 16 | "- URL (The server's URL with port)", 17 | "- Model (The model you want to generate the response)", 18 | "- Prompt (The prompt you send to the server to reply to)" 19 | ], 20 | "tags": [ 21 | "ollama", 22 | "ai", 23 | "llama", 24 | "llama2", 25 | "llama3", 26 | "chat", 27 | "bot" 28 | ], 29 | "authorIds": [], 30 | "dependencies": [], 31 | "eventsFunctions": [ 32 | { 33 | "description": "Sends the prompt string, the model string, and the stream boolean from the given structure.", 34 | "fullName": "Send prompt to a model", 35 | "functionType": "Action", 36 | "name": "Request", 37 | "sentence": "Send _PARAM2_ and _PARAM3_ to _PARAM1_, then store the response JSON in the variable \"Ollama_AI_JSON\"", 38 | "events": [ 39 | { 40 | "type": "BuiltinCommonInstructions::Standard", 41 | "conditions": [], 42 | "actions": [ 43 | { 44 | "type": { 45 | "value": "ModVarSceneTxt" 46 | }, 47 | "parameters": [ 48 | "Ollama_AI.model", 49 | "=", 50 | "Model" 51 | ] 52 | }, 53 | { 54 | "type": { 55 | "value": "ModVarSceneTxt" 56 | }, 57 | "parameters": [ 58 | "Ollama_AI.prompt", 59 | "=", 60 | "Prompt" 61 | ] 62 | }, 63 | { 64 | "type": { 65 | "value": "SetSceneVariableAsBoolean" 66 | }, 67 | "parameters": [ 68 | "Ollama_AI.stream", 69 | "False" 70 | ] 71 | }, 72 | { 73 | "type": { 74 | "value": "SendAsyncRequest" 75 | }, 76 | "parameters": [ 77 | "API_URL", 78 | "ToJSON(Ollama_AI)", 79 | "\"POST\"", 80 | "", 81 | "Ollama_AI_JSON", 82 | "Ollama_AI_Error" 83 | ] 84 | } 85 | ] 86 | } 87 | ], 88 | "parameters": [ 89 | { 90 | "description": "The URL where the Ollama model is hosted (e.g. http://localhost:11434/api/generate)", 91 | "longDescription": "The URL should be in this format: \"http://<ip address>:11434/api/generate\". If you are hosting and testing locally, use this URL: \"http://localhost:11434/api/generate\". Read the extension's GitHub issue on how to host your own server.", 92 | "name": "API_URL", 93 | "supplementaryInformation": "[\"http://localhost:11434/api/generate\"]", 94 | "type": "stringWithSelector" 95 | }, 96 | { 97 | "description": "The model to be used when generating a response", 98 | "longDescription": "The recommended one is \"llama3\", an older version is \"llama2\", but you can also customize the models and use those. Read the extension's GitHub issue on how to do this.", 99 | "name": "Model", 100 | "supplementaryInformation": "[\"llama3\",\"llama2\",\"codegemma\"]", 101 | "type": "stringWithSelector" 102 | }, 103 | { 104 | "description": "Your prompt to the AI, for example: \"Why is the sky blue?\"", 105 | "longDescription": "The response will be stored in JSON in the variable \"Ollama_AI_JSON\". After that, you can convert the JSON to a structure. You can see how you can do it in the example on the extension's GitHub.", 106 | "name": "Prompt", 107 | "type": "string" 108 | } 109 | ], 110 | "objectGroups": [] 111 | } 112 | ], 113 | "eventsBasedBehaviors": [], 114 | "eventsBasedObjects": [] 115 | } -------------------------------------------------------------------------------- /extensions/community/PauseFocusLost.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "User interface", 4 | "description": "When the game loses focus, either because you switch windows, open another tab in the browser or for any other reason, the game is paused and muted.\n\nWhen focus is regained, the game continues.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Pause when losing focus", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXBsYXktcGF1c2UiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMyw1VjE5TDExLDEyTTEzLDE5SDE2VjVIMTNNMTgsNVYxOUgyMVY1IiAvPjwvc3ZnPg==", 10 | "name": "PauseFocusLost", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/play-pause.svg", 12 | "shortDescription": "Pauses when focus is lost, restarts when focus is regained.", 13 | "version": "0.0.2", 14 | "tags": [ 15 | "Pause", 16 | "focus" 17 | ], 18 | "authorIds": [ 19 | "6gSSmzTTyVOHCx0jw8I9OFeI0aR2" 20 | ], 21 | "dependencies": [], 22 | "eventsFunctions": [ 23 | { 24 | "description": "Pause when game lost focus.", 25 | "fullName": "Active pause on blur", 26 | "functionType": "Action", 27 | "group": "", 28 | "name": "Active", 29 | "private": false, 30 | "sentence": "Pause when game lost focus", 31 | "events": [ 32 | { 33 | "type": "BuiltinCommonInstructions::JsCode", 34 | "inlineCode": "var sound_manager = runtimeScene.getGame().getSoundManager();\r\nvar volumen = sound_manager.getGlobalVolume();\r\n\r\nwindow.addEventListener('focus', function (event) {\r\nsound_manager.setGlobalVolume(volumen);\r\nruntimeScene.getGame().pause(false);\r\n});\r\n\r\nwindow.addEventListener('blur', function (event) {\r\nsound_manager.setGlobalVolume(0);\r\nruntimeScene.getGame().pause(true);\r\n});", 35 | "parameterObjects": "", 36 | "useStrict": true, 37 | "eventsSheetExpanded": false 38 | } 39 | ], 40 | "parameters": [], 41 | "objectGroups": [] 42 | } 43 | ], 44 | "eventsBasedBehaviors": [] 45 | } -------------------------------------------------------------------------------- /extensions/community/RandomColor.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "jeremy.leriche22@gmail.com", 3 | "category": "Advanced", 4 | "description": "Allows you to create a random color.\nTo use it, go in the text area to set a color and type `RandomColor::CreateRandomColor()`.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Random Color Generator", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZvcm1hdC1jb2xvci1maWxsIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE5LDExLjVDMTksMTEuNSAxNywxMy42NyAxNywxNUEyLDIgMCAwLDAgMTksMTdBMiwyIDAgMCwwIDIxLDE1QzIxLDEzLjY3IDE5LDExLjUgMTksMTEuNU01LjIxLDEwTDEwLDUuMjFMMTQuNzksMTBNMTYuNTYsOC45NEw3LjYyLDBMNi4yMSwxLjQxTDguNTksMy43OUwzLjQ0LDguOTRDMi44NSw5LjUgMi44NSwxMC40NyAzLjQ0LDExLjA2TDguOTQsMTYuNTZDOS4yMywxNi44NSA5LjYyLDE3IDEwLDE3QzEwLjM4LDE3IDEwLjc3LDE2Ljg1IDExLjA2LDE2LjU2TDE2LjU2LDExLjA2QzE3LjE1LDEwLjQ3IDE3LjE1LDkuNSAxNi41Niw4Ljk0WiIgLz48L3N2Zz4=", 10 | "name": "RandomColor", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/format-color-fill.svg", 12 | "shortDescription": "Create a random color for a scene, an object, or any other color input.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "random", 16 | "color" 17 | ], 18 | "authorIds": [ 19 | "4ng4NsgThidN4cnjobO4RdE5zKG2" 20 | ], 21 | "dependencies": [], 22 | "eventsFunctions": [ 23 | { 24 | "description": "Create a totally random color.", 25 | "fullName": "Random Color", 26 | "functionType": "StringExpression", 27 | "name": "CreateRandomColor", 28 | "private": false, 29 | "sentence": "", 30 | "events": [ 31 | { 32 | "disabled": false, 33 | "folded": false, 34 | "type": "BuiltinCommonInstructions::Standard", 35 | "conditions": [], 36 | "actions": [ 37 | { 38 | "type": { 39 | "inverted": false, 40 | "value": "SetReturnString" 41 | }, 42 | "parameters": [ 43 | "ToString(Random(255))+\";\"+ToString(Random(255))+\";\"+ToString(Random(255))" 44 | ], 45 | "subInstructions": [] 46 | } 47 | ], 48 | "events": [] 49 | } 50 | ], 51 | "parameters": [], 52 | "objectGroups": [] 53 | } 54 | ], 55 | "eventsBasedBehaviors": [] 56 | } -------------------------------------------------------------------------------- /extensions/community/Rotate13.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Silver-Streak", 3 | "category": "Advanced", 4 | "description": "By passing a string to this expression, you can go from:\n\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n\nto \n\n\"NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm\"\n\nThis can be used for basic secrets, passwords, or very insecure encryption of data.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Rotate a string 13 characters", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZvcm1hdC10ZXh0LXJvdGF0aW9uLW5vbmUiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjAuNSwxOEwxNy41LDIxVjE5SDVWMTdIMTcuNVYxNUwyMC41LDE4TTEwLjEzLDEwSDEzLjg4TDEyLDQuOTdMMTAuMTMsMTBNMTIuNzUsM0wxNy41LDE0SDE1LjQyTDE0LjUsMTEuODFIOS41TDguNTgsMTRINi41TDExLjI1LDNIMTIuNzVaIiAvPjwvc3ZnPg==", 10 | "name": "Rotate13", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/format-text-rotation-none.svg", 12 | "shortDescription": "This extension rotates all alphabetic characters in a string by 13 characters.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "rotate string", 16 | "rot13", 17 | "rotate13", 18 | "string" 19 | ], 20 | "authorIds": [ 21 | "8Ih1aa8f5gWUp4UB2BdhQ2iXWxJ3" 22 | ], 23 | "dependencies": [], 24 | "eventsFunctions": [ 25 | { 26 | "description": "Rotate String _PARAM1_ 13 Characters.", 27 | "fullName": "Rotate String 13 Characters", 28 | "functionType": "StringExpression", 29 | "name": "rot13", 30 | "private": false, 31 | "sentence": "", 32 | "events": [ 33 | { 34 | "disabled": false, 35 | "folded": false, 36 | "type": "BuiltinCommonInstructions::Standard", 37 | "conditions": [], 38 | "actions": [], 39 | "events": [] 40 | }, 41 | { 42 | "disabled": false, 43 | "folded": false, 44 | "type": "BuiltinCommonInstructions::JsCode", 45 | "inlineCode": "function rot13(s)\n {\n return (s ? s : this).split('').map(function(_)\n {\n if (!_.match(/[A-Za-z]/)) return _;\n var c = Math.floor(_.charCodeAt(0) / 97);\n var k = (_.toLowerCase().charCodeAt(0) - 83) % 26 || 26;\n return String.fromCharCode(k + ((c == 0) ? 64 : 96));\n }).join('');\n }\nvar ConvString = eventsFunctionContext.getArgument(\"RotStr\");\neventsFunctionContext.returnValue = rot13(ConvString);\n", 46 | "parameterObjects": "", 47 | "useStrict": true, 48 | "eventsSheetExpanded": false 49 | } 50 | ], 51 | "parameters": [ 52 | { 53 | "codeOnly": false, 54 | "defaultValue": "", 55 | "description": "String to Rotate", 56 | "longDescription": "", 57 | "name": "RotStr", 58 | "optional": false, 59 | "supplementaryInformation": "", 60 | "type": "string" 61 | } 62 | ], 63 | "objectGroups": [] 64 | } 65 | ], 66 | "eventsBasedBehaviors": [] 67 | } -------------------------------------------------------------------------------- /extensions/reviewed/AdvancedP2PEventHandling.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "category": "Network", 4 | "description": "Allows handling all events at once instead of one per frame. \n\n### Usage:\n![Screenshot of the way to use the extension](https://i.imgur.com/DkYfM7W.png)", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Advanced p2p event handling", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQoJLnN0MXtmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEwO30NCjwvc3R5bGU+DQo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMjMsMTVIOWMtMS4xLDAtMi0wLjktMi0yVjRjMC0xLjEsMC45LTIsMi0yaDE0YzEuMSwwLDIsMC45LDIsMnY5QzI1LDE0LjEsMjQuMSwxNSwyMywxNXoiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSI3IiB5MT0iNiIgeDI9IjI1IiB5Mj0iNiIvPg0KPGxpbmUgY2xhc3M9InN0MCIgeDE9IjExIiB5MT0iNSIgeDI9IjExIiB5Mj0iMiIvPg0KPHJlY3QgeD0iMyIgeT0iMjMiIGNsYXNzPSJzdDAiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiLz4NCjxyZWN0IHg9IjEzIiB5PSIyMyIgY2xhc3M9InN0MCIgd2lkdGg9IjYiIGhlaWdodD0iNiIvPg0KPHJlY3QgeD0iMjMiIHk9IjIzIiBjbGFzcz0ic3QwIiB3aWR0aD0iNiIgaGVpZ2h0PSI2Ii8+DQo8cG9seWxpbmUgY2xhc3M9InN0MCIgcG9pbnRzPSI2LDIzIDYsMTkgMjYsMTkgMjYsMjMgIi8+DQo8bGluZSBjbGFzcz0ic3QwIiB4MT0iMTYiIHkxPSIxNSIgeDI9IjE2IiB5Mj0iMjMiLz4NCjwvc3ZnPg0K", 10 | "name": "AdvancedP2PEventHandling", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Applications and Programming/Applications and Programming_sitemap_map_ux_application.svg", 12 | "shortDescription": "Allows handling all received P2P events at once instead of one per frame. It is more complex but also potentially more performant.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "p2p", 16 | "performance", 17 | "advanced" 18 | ], 19 | "authorIds": [ 20 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 21 | ], 22 | "dependencies": [], 23 | "eventsFunctions": [ 24 | { 25 | "description": "Marks the event as handled, to go on to the next.", 26 | "fullName": "Dismiss event", 27 | "functionType": "Action", 28 | "name": "DissmissEvent", 29 | "private": false, 30 | "sentence": "Dismiss event _PARAM1_ as handled", 31 | "events": [ 32 | { 33 | "disabled": false, 34 | "folded": false, 35 | "type": "BuiltinCommonInstructions::JsCode", 36 | "inlineCode": "if (!gdjs.evtTools.p2p) return;\nif (gdjs.evtTools.p2p.getEvent)\n gdjs.evtTools.p2p\n .getEvent()\n .popData();\n// Pre beta104 compatibility\nelse {\n const event = gdjs.evtTools.p2p.lastEventData[eventsFunctionContext.getArgument(\"eventName\")];\n if (Array.isArray(event) && event.length > 0) event.pop();\n};\n", 37 | "parameterObjects": "", 38 | "useStrict": true, 39 | "eventsSheetExpanded": true 40 | } 41 | ], 42 | "parameters": [ 43 | { 44 | "codeOnly": false, 45 | "defaultValue": "", 46 | "description": "The event to dismiss", 47 | "longDescription": "", 48 | "name": "eventName", 49 | "optional": false, 50 | "supplementaryInformation": "", 51 | "type": "string" 52 | } 53 | ], 54 | "objectGroups": [] 55 | } 56 | ], 57 | "eventsBasedBehaviors": [] 58 | } -------------------------------------------------------------------------------- /extensions/reviewed/BackButton.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "category": "Input", 4 | "description": "Prevents the Android phone/tablet back button from quitting the game and provides a condition to check when it's pressed (to allow customising its behavior). Works only for games published on Android.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Android back button", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWtleWJvYXJkLWJhY2tzcGFjZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0yMSwxMUg2LjgzTDEwLjQxLDcuNDFMOSw2TDMsMTJMOSwxOEwxMC40MSwxNi41OEw2LjgzLDEzSDIxVjExWiIgLz48L3N2Zz4=", 10 | "name": "BackButton", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/keyboard-backspace.svg", 12 | "shortDescription": "Allow to customize the behavior of the Android back button.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "back", 16 | "mobile", 17 | "button", 18 | "input" 19 | ], 20 | "authorIds": [ 21 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 22 | ], 23 | "dependencies": [], 24 | "eventsFunctions": [ 25 | { 26 | "description": "", 27 | "fullName": "", 28 | "functionType": "Action", 29 | "name": "onFirstSceneLoaded", 30 | "private": false, 31 | "sentence": "", 32 | "events": [ 33 | { 34 | "disabled": false, 35 | "folded": false, 36 | "type": "BuiltinCommonInstructions::JsCode", 37 | "inlineCode": "gdjs.evtTools.back_button = {\n triggered: false,\n _popStateListener: (event) => {\n gdjs.evtTools.back_button.triggered = true;\n history.pushState(\"\", \"\"); // Push a new fake state as the old one was popped\n }\n};\n\n// Handle back button on the web\nhistory.pushState(\"\", \"\"); // Push a fake state to prevent switching page when clicking on back\nwindow.addEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n\n// Handle back button on cordova\ndocument.addEventListener(\"backbutton\", e => {\n e.preventDefault();\n gdjs.evtTools.back_button.triggered = true;\n}, false); \n", 38 | "parameterObjects": "", 39 | "useStrict": true, 40 | "eventsSheetExpanded": false 41 | } 42 | ], 43 | "parameters": [], 44 | "objectGroups": [] 45 | }, 46 | { 47 | "description": "Triggers whenever the player presses the back button.", 48 | "fullName": "Back button is pressed", 49 | "functionType": "Condition", 50 | "name": "onBackButtonPressed", 51 | "private": false, 52 | "sentence": "Back button is pressed", 53 | "events": [ 54 | { 55 | "disabled": false, 56 | "folded": false, 57 | "type": "BuiltinCommonInstructions::JsCode", 58 | "inlineCode": "eventsFunctionContext.returnValue = gdjs.evtTools.back_button.triggered;\n", 59 | "parameterObjects": "", 60 | "useStrict": true, 61 | "eventsSheetExpanded": false 62 | } 63 | ], 64 | "parameters": [], 65 | "objectGroups": [] 66 | }, 67 | { 68 | "description": "This simulates the normal action of the back button. \nThis action will quit the app when in a mobile app, and go back to the previous page when in a web browser.", 69 | "fullName": "Trigger back button", 70 | "functionType": "Action", 71 | "name": "doDefault", 72 | "private": false, 73 | "sentence": "Simulate back button press", 74 | "events": [ 75 | { 76 | "disabled": false, 77 | "folded": false, 78 | "type": "BuiltinCommonInstructions::JsCode", 79 | "inlineCode": "// Close the app on cordova, as this is the default behavior\nif (navigator.app) {\n navigator.app.exitApp();\n} else if (navigator.device && navigator.device.exitApp) {\n navigator.device.exitApp();\n} else {\n // Go to previous page as it is the default on browsers\n // Remove the listener so new fake states don't get pushed\n window.removeEventListener('popstate', gdjs.evtTools.back_button._popStateListener);\n history.back(); // Remove the state that prevents going back\n history.back(); // Actually go back\n}\n", 80 | "parameterObjects": "", 81 | "useStrict": true, 82 | "eventsSheetExpanded": false 83 | } 84 | ], 85 | "parameters": [], 86 | "objectGroups": [] 87 | }, 88 | { 89 | "description": "", 90 | "fullName": "", 91 | "functionType": "Action", 92 | "name": "onScenePostEvents", 93 | "private": false, 94 | "sentence": "", 95 | "events": [ 96 | { 97 | "disabled": false, 98 | "folded": false, 99 | "type": "BuiltinCommonInstructions::JsCode", 100 | "inlineCode": "gdjs.evtTools.back_button.triggered = false;\n", 101 | "parameterObjects": "", 102 | "useStrict": true, 103 | "eventsSheetExpanded": false 104 | } 105 | ], 106 | "parameters": [], 107 | "objectGroups": [] 108 | } 109 | ], 110 | "eventsBasedBehaviors": [] 111 | } 112 | -------------------------------------------------------------------------------- /extensions/reviewed/BaseConversion.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ahnaf30e", 3 | "category": "Advanced", 4 | "description": "Adds expressions to convert numbers to a different base and back. Can be used to convert hexadecimal / binary representations of numbers to decimal numbers and vice-versa.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Base conversion", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWhleGFkZWNpbWFsIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTcgN0M1LjkgNyA1IDcuOSA1IDlWMTVDNSAxNi4xMSA1LjkgMTcgNyAxN0g5QzEwLjExIDE3IDExIDE2LjExIDExIDE1VjlDMTEgNy45IDEwLjExIDcgOSA3SDdNNyA5SDlWMTVIN1Y5TTE3LjYgMTdMMTUuNSAxNC45TDEzLjQgMTdMMTIgMTUuNkwxNC4xIDEzLjVMMTIgMTEuNEwxMy40IDEwTDE1LjUgMTIuMUwxNy42IDEwTDE5IDExLjRMMTYuOSAxMy41TDE5IDE1LjZMMTcuNiAxN1oiIC8+PC9zdmc+", 10 | "name": "BaseConversion", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/hexadecimal.svg", 12 | "shortDescription": "Provides conversion expressions for numbers in different bases.", 13 | "version": "1.0.1", 14 | "tags": [ 15 | "binary", 16 | "numbers", 17 | "number", 18 | "base", 19 | "hex", 20 | "decimal" 21 | ], 22 | "authorIds": [ 23 | "onPsboRtDkUHNOsx7OPr8R8G1oj2" 24 | ], 25 | "dependencies": [], 26 | "eventsFunctions": [ 27 | { 28 | "description": "Converts a string representing a number in a different base to a decimal number.", 29 | "fullName": "Convert to decimal", 30 | "functionType": "Expression", 31 | "name": "ToDecimal", 32 | "private": false, 33 | "sentence": "", 34 | "events": [ 35 | { 36 | "disabled": false, 37 | "folded": false, 38 | "type": "BuiltinCommonInstructions::JsCode", 39 | "inlineCode": "const convString = eventsFunctionContext.getArgument(\"String\");\nconst convBase = eventsFunctionContext.getArgument(\"Base\");\neventsFunctionContext.returnValue = parseInt(convString, convBase);\n", 40 | "parameterObjects": "", 41 | "useStrict": true, 42 | "eventsSheetExpanded": true 43 | } 44 | ], 45 | "parameters": [ 46 | { 47 | "codeOnly": false, 48 | "defaultValue": "", 49 | "description": "String representing a number", 50 | "longDescription": "", 51 | "name": "String", 52 | "optional": false, 53 | "supplementaryInformation": "", 54 | "type": "string" 55 | }, 56 | { 57 | "codeOnly": false, 58 | "defaultValue": "", 59 | "description": "The base the number in the string is in", 60 | "longDescription": "", 61 | "name": "Base", 62 | "optional": false, 63 | "supplementaryInformation": "", 64 | "type": "expression" 65 | } 66 | ], 67 | "objectGroups": [] 68 | }, 69 | { 70 | "description": "Converts a number to a trsing representing it in another base.", 71 | "fullName": "Convert to different base", 72 | "functionType": "StringExpression", 73 | "name": "ToBase", 74 | "private": false, 75 | "sentence": "", 76 | "events": [ 77 | { 78 | "disabled": false, 79 | "folded": false, 80 | "type": "BuiltinCommonInstructions::JsCode", 81 | "inlineCode": "const convNum = eventsFunctionContext.getArgument(\"Num\");\nconst convBase = eventsFunctionContext.getArgument(\"Base\");\neventsFunctionContext.returnValue = convNum.toString(convBase);\n", 82 | "parameterObjects": "", 83 | "useStrict": true, 84 | "eventsSheetExpanded": true 85 | } 86 | ], 87 | "parameters": [ 88 | { 89 | "codeOnly": false, 90 | "defaultValue": "", 91 | "description": "Number to convert", 92 | "longDescription": "", 93 | "name": "Num", 94 | "optional": false, 95 | "supplementaryInformation": "", 96 | "type": "expression" 97 | }, 98 | { 99 | "codeOnly": false, 100 | "defaultValue": "", 101 | "description": "The base to convert the number to", 102 | "longDescription": "", 103 | "name": "Base", 104 | "optional": false, 105 | "supplementaryInformation": "", 106 | "type": "expression" 107 | } 108 | ], 109 | "objectGroups": [] 110 | } 111 | ], 112 | "eventsBasedBehaviors": [] 113 | } -------------------------------------------------------------------------------- /extensions/reviewed/FlashLayer.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Tristan Rhodes (tristan@victrisgames.com)", 3 | "category": "Visual effect", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Flash layer", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZsYXNoLW91dGxpbmUiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNywySDE3TDEzLjUsOUgxN0wxMCwyMlYxNEg3VjJNOSw0VjEySDEyVjE0LjY2TDE0LDExSDEwLjI0TDEzLjc2LDRIOVoiIC8+PC9zdmc+", 9 | "name": "FlashLayer", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/flash-outline.svg", 11 | "shortDescription": "Make a layer visible for a specified duration, and then hide the layer.", 12 | "version": "0.3.0", 13 | "description": [ 14 | "Useful to make a temporary effect (flash on hit, flickering lights, lightning flash, show text like Batman and Robin \"Bam!\", etc)", 15 | "", 16 | "It is recommended to select a layer on the top, and one that is hidden by default." 17 | ], 18 | "origin": { 19 | "identifier": "FlashLayer", 20 | "name": "gdevelop-extension-store" 21 | }, 22 | "tags": [ 23 | "effect", 24 | "vfx", 25 | "layer", 26 | "flash" 27 | ], 28 | "authorIds": [ 29 | "gqDaZjCfevOOxBYkK6zlhtZnXCg1" 30 | ], 31 | "dependencies": [], 32 | "globalVariables": [], 33 | "sceneVariables": [], 34 | "eventsFunctions": [ 35 | { 36 | "description": "Make a layer visible for a specified duration, and then hide the layer.", 37 | "fullName": "Flash layer", 38 | "functionType": "Action", 39 | "name": "FlashLayer", 40 | "sentence": "Make layer _PARAM1_ visible for _PARAM2_ seconds", 41 | "events": [ 42 | { 43 | "type": "BuiltinCommonInstructions::Standard", 44 | "conditions": [ 45 | { 46 | "type": { 47 | "inverted": true, 48 | "value": "LayerVisible" 49 | }, 50 | "parameters": [ 51 | "", 52 | "Layer" 53 | ] 54 | } 55 | ], 56 | "actions": [ 57 | { 58 | "type": { 59 | "value": "ShowLayer" 60 | }, 61 | "parameters": [ 62 | "", 63 | "Layer" 64 | ] 65 | }, 66 | { 67 | "type": { 68 | "value": "Wait" 69 | }, 70 | "parameters": [ 71 | "Duration" 72 | ] 73 | }, 74 | { 75 | "type": { 76 | "value": "HideLayer" 77 | }, 78 | "parameters": [ 79 | "", 80 | "Layer" 81 | ] 82 | } 83 | ] 84 | } 85 | ], 86 | "parameters": [ 87 | { 88 | "description": "Layer", 89 | "name": "Layer", 90 | "type": "layer" 91 | }, 92 | { 93 | "description": "Duration (in seconds)", 94 | "name": "Duration", 95 | "type": "expression" 96 | } 97 | ], 98 | "objectGroups": [] 99 | } 100 | ], 101 | "eventsBasedBehaviors": [], 102 | "eventsBasedObjects": [] 103 | } -------------------------------------------------------------------------------- /extensions/reviewed/InternetConnectivity.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gabriel (@TheGemDev)", 3 | "category": "Network", 4 | "description": "Checks if the device running the game is connected to the internet.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Internet Connectivity ", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWFjY2Vzcy1wb2ludC1uZXR3b3JrIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTQuOTMsMy45M0MzLjEyLDUuNzQgMiw4LjI0IDIsMTFDMiwxMy43NiAzLjEyLDE2LjI2IDQuOTMsMTguMDdMNi4zNCwxNi42NkM0Ljg5LDE1LjIyIDQsMTMuMjIgNCwxMUM0LDguNzkgNC44OSw2Ljc4IDYuMzQsNS4zNEw0LjkzLDMuOTNNMTkuMDcsMy45M0wxNy42Niw1LjM0QzE5LjExLDYuNzggMjAsOC43OSAyMCwxMUMyMCwxMy4yMiAxOS4xMSwxNS4yMiAxNy42NiwxNi42NkwxOS4wNywxOC4wN0MyMC44OCwxNi4yNiAyMiwxMy43NiAyMiwxMUMyMiw4LjI0IDIwLjg4LDUuNzQgMTkuMDcsMy45M003Ljc2LDYuNzZDNi42Nyw3Ljg1IDYsOS4zNSA2LDExQzYsMTIuNjUgNi42NywxNC4xNSA3Ljc2LDE1LjI0TDkuMTcsMTMuODNDOC40NSwxMy4xMSA4LDEyLjExIDgsMTFDOCw5Ljg5IDguNDUsOC44OSA5LjE3LDguMTdMNy43Niw2Ljc2TTE2LjI0LDYuNzZMMTQuODMsOC4xN0MxNS41NSw4Ljg5IDE2LDkuODkgMTYsMTFDMTYsMTIuMTEgMTUuNTUsMTMuMTEgMTQuODMsMTMuODNMMTYuMjQsMTUuMjRDMTcuMzMsMTQuMTUgMTgsMTIuNjUgMTgsMTFDMTgsOS4zNSAxNy4zMyw3Ljg1IDE2LjI0LDYuNzZNMTIsOUEyLDIgMCAwLDAgMTAsMTFBMiwyIDAgMCwwIDEyLDEzQTIsMiAwIDAsMCAxNCwxMUEyLDIgMCAwLDAgMTIsOU0xMSwxNVYxOUgxMEExLDEgMCAwLDAgOSwyMEgyVjIySDlBMSwxIDAgMCwwIDEwLDIzSDE0QTEsMSAwIDAsMCAxNSwyMkgyMlYyMEgxNUExLDEgMCAwLDAgMTQsMTlIMTNWMTVIMTFaIiAvPjwvc3ZnPg==", 10 | "name": "InternetConnectivity", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/access-point-network.svg", 12 | "shortDescription": "Checks if the device running the game is connected to the internet.", 13 | "version": "0.0.1", 14 | "tags": [ 15 | "javascript", 16 | "internet", 17 | "network connection", 18 | "online" 19 | ], 20 | "authorIds": [ 21 | "jy7FXnGX0ZZcWfrAI9YuQaeIphi1" 22 | ], 23 | "dependencies": [], 24 | "eventsFunctions": [ 25 | { 26 | "description": "Checks if the device is connected to the internet.", 27 | "fullName": "Is the device online?", 28 | "functionType": "Condition", 29 | "name": "IsDeviceOnline", 30 | "private": false, 31 | "sentence": "The device is online", 32 | "events": [ 33 | { 34 | "disabled": false, 35 | "folded": false, 36 | "type": "BuiltinCommonInstructions::JsCode", 37 | "inlineCode": "eventsFunctionContext.returnValue = typeof navigator !== \"undefined\" && navigator.onLine;\n\n", 38 | "parameterObjects": "", 39 | "useStrict": true, 40 | "eventsSheetExpanded": false 41 | } 42 | ], 43 | "parameters": [], 44 | "objectGroups": [] 45 | } 46 | ], 47 | "eventsBasedBehaviors": [] 48 | } -------------------------------------------------------------------------------- /extensions/reviewed/IsOnScreen.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Silver-Streak, @Bouh, Tristan Rhodes", 3 | "category": "Game mechanic", 4 | "extensionNamespace": "", 5 | "fullName": "Object \"Is On Screen\" Detection", 6 | "gdevelopVersion": "", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLW1vbml0b3Itc2NyZWVuc2hvdCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik05LDZINVYxMEg3VjhIOU0xOSwxMEgxN1YxMkgxNVYxNEgxOU0yMSwxNkgzVjRIMjFNMjEsMkgzQzEuODksMiAxLDIuODkgMSw0VjE2QTIsMiAwIDAsMCAzLDE4SDEwVjIwSDhWMjJIMTZWMjBIMTRWMThIMjFBMiwyIDAgMCwwIDIzLDE2VjRDMjMsMi44OSAyMi4xLDIgMjEsMiIgLz48L3N2Zz4=", 9 | "name": "IsOnScreen", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/monitor-screenshot.svg", 11 | "shortDescription": "This adds a condition to detect if an object is on screen based off its current layer.", 12 | "version": "1.2.2", 13 | "description": [ 14 | "This extension adds conditions to check if an object is located within the visible portion of its layer's camera. The condition also allows for specifying padding to the virtual screen border.", 15 | "", 16 | "Note that this does not take into account any object visibility, such as being hidden or 0 opacity, but can be combined with those existing conditions." 17 | ], 18 | "origin": { 19 | "identifier": "IsOnScreen", 20 | "name": "gdevelop-extension-store" 21 | }, 22 | "tags": [ 23 | "is on screen", 24 | "condition", 25 | "visible", 26 | "hide", 27 | "screen" 28 | ], 29 | "authorIds": [ 30 | "2OwwM8ToR9dx9RJ2sAKTcrLmCB92", 31 | "8Ih1aa8f5gWUp4UB2BdhQ2iXWxJ3", 32 | "gqDaZjCfevOOxBYkK6zlhtZnXCg1" 33 | ], 34 | "dependencies": [], 35 | "globalVariables": [], 36 | "sceneVariables": [], 37 | "eventsFunctions": [], 38 | "eventsBasedBehaviors": [ 39 | { 40 | "description": "This behavior provides a condition to check if the object is located within the visible portion of its layer's camera. The condition also allows for specifying padding to the virtual screen border.\nNote that object visibility, such as being hidden or 0 opacity, is not considered (but you can use those existing conditions in addition to this behavior).", 41 | "fullName": "Is on screen", 42 | "name": "InOnScreen", 43 | "objectType": "", 44 | "quickCustomizationVisibility": "hidden", 45 | "eventsFunctions": [ 46 | { 47 | "description": "Checks if an object position is within the viewport of its layer.", 48 | "fullName": "Is on screen", 49 | "functionType": "Condition", 50 | "name": "IsOnScreen", 51 | "sentence": "_PARAM0_ is on screen (padded by _PARAM2_ pixels)", 52 | "events": [ 53 | { 54 | "type": "BuiltinCommonInstructions::JsCode", 55 | "inlineCode": [ 56 | "/*", 57 | "Get the object layer, convert the position from this layer to the screen coordinates.", 58 | "Get the point on each side on the object on screen, and compare with the screen area.", 59 | "", 60 | "This way even if the camera has a rotation or custom scale the object is always compared to the screen area.", 61 | "*/", 62 | "", 63 | "", 64 | "// Get the layer of the object.", 65 | "const object = objects[0];", 66 | "const layer = runtimeScene.getLayer(object.getLayer());", 67 | "", 68 | "// Get the aabb of the object on his layer.", 69 | "const aabb = object.getVisibilityAABB();", 70 | "", 71 | "// Get the layer to convert the coordinates of the AABB to the screen coordinates", 72 | "const topLeft = layer.convertInverseCoords(aabb.min[0], aabb.min[1]);", 73 | "const topRight = layer.convertInverseCoords(aabb.max[0], aabb.min[1]);", 74 | "const bottomRight = layer.convertInverseCoords(aabb.max[0], aabb.max[1]);", 75 | "const bottomLeft = layer.convertInverseCoords(aabb.min[0], aabb.max[1]);", 76 | "", 77 | "// Get the points on each side of the object on screen.", 78 | "const posLeftObjectOnScreen = Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]);", 79 | "const posRightObjectOnScreen = Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]);", 80 | "const posUpObjectOnScreen = Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]);", 81 | "const posDownObjectOnScreen = Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]);", 82 | "", 83 | "const padding = eventsFunctionContext.getArgument(\"Padding\");", 84 | "", 85 | "if (", 86 | " !(posLeftObjectOnScreen - padding > runtimeScene.getGame().getGameResolutionWidth() ||", 87 | " posUpObjectOnScreen - padding > runtimeScene.getGame().getGameResolutionHeight() ||", 88 | " posRightObjectOnScreen + padding < 0 ||", 89 | " posDownObjectOnScreen + padding < 0", 90 | " )", 91 | ") {", 92 | " eventsFunctionContext.returnValue = true;", 93 | "}", 94 | "" 95 | ], 96 | "parameterObjects": "Object", 97 | "useStrict": true, 98 | "eventsSheetExpanded": true 99 | } 100 | ], 101 | "parameters": [ 102 | { 103 | "description": "Object", 104 | "name": "Object", 105 | "type": "object" 106 | }, 107 | { 108 | "description": "Behavior", 109 | "name": "Behavior", 110 | "supplementaryInformation": "IsOnScreen::InOnScreen", 111 | "type": "behavior" 112 | }, 113 | { 114 | "description": "Padding (in pixels)", 115 | "longDescription": "Number of pixels to pad the screen border. Zero by default. A negative value goes inside the screen, a positive value go outside.", 116 | "name": "Padding", 117 | "type": "expression" 118 | } 119 | ], 120 | "objectGroups": [ 121 | { 122 | "name": "Group", 123 | "objects": [] 124 | } 125 | ] 126 | } 127 | ], 128 | "propertyDescriptors": [], 129 | "sharedPropertyDescriptors": [] 130 | } 131 | ], 132 | "eventsBasedObjects": [] 133 | } -------------------------------------------------------------------------------- /extensions/reviewed/Language.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gabriel (@TheGemDev) & Maury Dev (@MauryDev)", 3 | "category": "User interface", 4 | "description": "Get the preferred language of the user, set on their browser or device.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Language", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWVhcnRoIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE3LjksMTcuMzlDMTcuNjQsMTYuNTkgMTYuODksMTYgMTYsMTZIMTVWMTNBMSwxIDAgMCwwIDE0LDEySDhWMTBIMTBBMSwxIDAgMCwwIDExLDlWN0gxM0EyLDIgMCAwLDAgMTUsNVY0LjU5QzE3LjkzLDUuNzcgMjAsOC42NCAyMCwxMkMyMCwxNC4wOCAxOS4yLDE1Ljk3IDE3LjksMTcuMzlNMTEsMTkuOTNDNy4wNSwxOS40NCA0LDE2LjA4IDQsMTJDNCwxMS4zOCA0LjA4LDEwLjc4IDQuMjEsMTAuMjFMOSwxNVYxNkEyLDIgMCAwLDAgMTEsMThNMTIsMkExMCwxMCAwIDAsMCAyLDEyQTEwLDEwIDAgMCwwIDEyLDIyQTEwLDEwIDAgMCwwIDIyLDEyQTEwLDEwIDAgMCwwIDEyLDJaIiAvPjwvc3ZnPg==", 10 | "name": "Language", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/earth.svg", 12 | "shortDescription": "Get the preferred language of the user, set on their browser or device.", 13 | "version": "0.0.1", 14 | "tags": [ 15 | "javascript", 16 | "language" 17 | ], 18 | "authorIds": [ 19 | "jy7FXnGX0ZZcWfrAI9YuQaeIphi1" 20 | ], 21 | "dependencies": [], 22 | "eventsFunctions": [ 23 | { 24 | "description": "Returns a string representing the preferred language of the user.\nThe format represents the language first, and usually the country where it's used. For example: \"en\" (English), \"en-US\" (English as used in the United States), \"en-GB\" (United Kingdom English), \"es\" (Spanish), \"zh-CN\" (Chinese as used in China), etc.", 25 | "fullName": "Language", 26 | "functionType": "StringExpression", 27 | "name": "Language", 28 | "private": false, 29 | "sentence": "", 30 | "events": [ 31 | { 32 | "disabled": false, 33 | "folded": false, 34 | "type": "BuiltinCommonInstructions::JsCode", 35 | "inlineCode": "eventsFunctionContext.returnValue = navigator.language || \"\";", 36 | "parameterObjects": "", 37 | "useStrict": true, 38 | "eventsSheetExpanded": false 39 | } 40 | ], 41 | "parameters": [], 42 | "objectGroups": [] 43 | } 44 | ], 45 | "eventsBasedBehaviors": [] 46 | } -------------------------------------------------------------------------------- /extensions/reviewed/LinearMovement.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "@4ian", 3 | "category": "Movement", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Linear Movement", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXJheS1zdGFydC1hcnJvdyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0yMywxMkwxOSwxNlYxM0g2LjgzQzYuNDIsMTQuMTcgNS4zMSwxNSA0LDE1QTMsMyAwIDAsMSAxLDEyQTMsMyAwIDAsMSA0LDlDNS4zMSw5IDYuNDIsOS44MyA2LjgzLDExSDE5VjhMMjMsMTJaIiAvPjwvc3ZnPg==", 9 | "name": "LinearMovement", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/ray-start-arrow.svg", 11 | "shortDescription": "Move objects on a straight line.", 12 | "version": "0.1.0", 13 | "description": [ 14 | "Move objects on a straight line or according to their angle.", 15 | "", 16 | "It can be used for simple enemies or bullets. It's usually not adapted for players (other behaviors are a better choice) or bullets fired with the actions provided by the \"Fire Bullet\" behavior (these bullets are already moved using a force)." 17 | ], 18 | "origin": { 19 | "identifier": "LinearMovement", 20 | "name": "gdevelop-extension-store" 21 | }, 22 | "tags": [ 23 | "line", 24 | "movement", 25 | "linear" 26 | ], 27 | "authorIds": [ 28 | "wWP8BSlAW0UP4NeaHa2LcmmDzmH2", 29 | "dt0tRnf2kHWJnjkrpnzTzNj9Yc63" 30 | ], 31 | "dependencies": [], 32 | "globalVariables": [], 33 | "sceneVariables": [], 34 | "eventsFunctions": [], 35 | "eventsBasedBehaviors": [ 36 | { 37 | "description": "Move objects on a straight line.", 38 | "fullName": "Linear movement", 39 | "name": "LinearMovement", 40 | "objectType": "", 41 | "eventsFunctions": [ 42 | { 43 | "fullName": "", 44 | "functionType": "Action", 45 | "name": "doStepPreEvents", 46 | "sentence": "", 47 | "events": [ 48 | { 49 | "type": "BuiltinCommonInstructions::Standard", 50 | "conditions": [], 51 | "actions": [ 52 | { 53 | "type": { 54 | "value": "AddForceXY" 55 | }, 56 | "parameters": [ 57 | "Object", 58 | "SpeedX", 59 | "SpeedY", 60 | "" 61 | ] 62 | } 63 | ] 64 | } 65 | ], 66 | "parameters": [ 67 | { 68 | "description": "Object", 69 | "name": "Object", 70 | "type": "object" 71 | }, 72 | { 73 | "description": "Behavior", 74 | "name": "Behavior", 75 | "supplementaryInformation": "LinearMovement::LinearMovement", 76 | "type": "behavior" 77 | } 78 | ], 79 | "objectGroups": [] 80 | } 81 | ], 82 | "propertyDescriptors": [ 83 | { 84 | "value": "0", 85 | "type": "Number", 86 | "unit": "PixelSpeed", 87 | "label": "Speed on X axis", 88 | "description": "", 89 | "group": "", 90 | "extraInformation": [], 91 | "name": "SpeedX" 92 | }, 93 | { 94 | "value": "0", 95 | "type": "Number", 96 | "unit": "PixelSpeed", 97 | "label": "Speed on Y axis", 98 | "description": "", 99 | "group": "", 100 | "extraInformation": [], 101 | "name": "SpeedY" 102 | } 103 | ], 104 | "sharedPropertyDescriptors": [] 105 | }, 106 | { 107 | "description": "Move objects ahead according to their angle.", 108 | "fullName": "Linear movement by angle", 109 | "name": "LinearMovementByAngle", 110 | "objectType": "", 111 | "eventsFunctions": [ 112 | { 113 | "fullName": "", 114 | "functionType": "Action", 115 | "name": "doStepPreEvents", 116 | "sentence": "", 117 | "events": [ 118 | { 119 | "type": "BuiltinCommonInstructions::Standard", 120 | "conditions": [], 121 | "actions": [ 122 | { 123 | "type": { 124 | "value": "AddForceAL" 125 | }, 126 | "parameters": [ 127 | "Object", 128 | "Object.Angle()", 129 | "Speed", 130 | "" 131 | ] 132 | } 133 | ] 134 | } 135 | ], 136 | "parameters": [ 137 | { 138 | "description": "Object", 139 | "name": "Object", 140 | "type": "object" 141 | }, 142 | { 143 | "description": "Behavior", 144 | "name": "Behavior", 145 | "supplementaryInformation": "LinearMovement::LinearMovementByAngle", 146 | "type": "behavior" 147 | } 148 | ], 149 | "objectGroups": [] 150 | } 151 | ], 152 | "propertyDescriptors": [ 153 | { 154 | "value": "200", 155 | "type": "Number", 156 | "unit": "PixelSpeed", 157 | "label": "Speed", 158 | "description": "", 159 | "group": "", 160 | "extraInformation": [], 161 | "name": "Speed" 162 | } 163 | ], 164 | "sharedPropertyDescriptors": [] 165 | } 166 | ], 167 | "eventsBasedObjects": [] 168 | } 169 | -------------------------------------------------------------------------------- /extensions/reviewed/Share.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "category": "User interface", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Share dialog and sharing options", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXNoYXJlLXZhcmlhbnQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTgsMTYuMDhDMTcuMjQsMTYuMDggMTYuNTYsMTYuMzggMTYuMDQsMTYuODVMOC45MSwxMi43QzguOTYsMTIuNDcgOSwxMi4yNCA5LDEyQzksMTEuNzYgOC45NiwxMS41MyA4LjkxLDExLjNMMTUuOTYsNy4xOUMxNi41LDcuNjkgMTcuMjEsOCAxOCw4QTMsMyAwIDAsMCAyMSw1QTMsMyAwIDAsMCAxOCwyQTMsMyAwIDAsMCAxNSw1QzE1LDUuMjQgMTUuMDQsNS40NyAxNS4wOSw1LjdMOC4wNCw5LjgxQzcuNSw5LjMxIDYuNzksOSA2LDlBMywzIDAgMCwwIDMsMTJBMywzIDAgMCwwIDYsMTVDNi43OSwxNSA3LjUsMTQuNjkgOC4wNCwxNC4xOUwxNS4xNiwxOC4zNEMxNS4xMSwxOC41NSAxNS4wOCwxOC43NyAxNS4wOCwxOUMxNS4wOCwyMC42MSAxNi4zOSwyMS45MSAxOCwyMS45MUMxOS42MSwyMS45MSAyMC45MiwyMC42MSAyMC45MiwxOUEyLjkyLDIuOTIgMCAwLDAgMTgsMTYuMDhaIiAvPjwvc3ZnPg==", 9 | "name": "Share", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/share-variant.svg", 11 | "shortDescription": "Allows to share content via the system share dialog. Works only on mobile (browser or mobile app).", 12 | "version": "0.0.1", 13 | "description": [ 14 | "Actions and conditions to share a text and/or URL via the operating system share dialog.", 15 | "", 16 | "This will work for Android and iOS on browsers (Google Chrome, Safari...) and on mobile apps." 17 | ], 18 | "tags": [ 19 | "share", 20 | "mobile" 21 | ], 22 | "authorIds": [ 23 | "wWP8BSlAW0UP4NeaHa2LcmmDzmH2", 24 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 25 | ], 26 | "dependencies": [ 27 | { 28 | "exportName": "cordova-plugin-web-share", 29 | "name": "cordova-plugin-web-share", 30 | "type": "cordova", 31 | "version": "https://github.com/arthuro555/cordova-webshare-api.git" 32 | } 33 | ], 34 | "eventsFunctions": [ 35 | { 36 | "async": true, 37 | "description": "Share a link or text via another app using the system share dialog.", 38 | "fullName": "Share", 39 | "functionType": "Action", 40 | "name": "Share", 41 | "sentence": "Share text: _PARAM1_ and url: _PARAM2_ with title _PARAM3_", 42 | "events": [ 43 | { 44 | "type": "BuiltinCommonInstructions::JsCode", 45 | "inlineCode": [ 46 | "gdjs._shareExtension = gdjs._shareExtension || {\r", 47 | " lastShareResult: '',\r", 48 | "};\r", 49 | "\r", 50 | "if (!navigator.share) {\r", 51 | " gdjs._shareExtension.lastShareResult = 'unsupported';\r", 52 | " eventsFunctionContext.task.resolve()\r", 53 | " return;\r", 54 | "}\r", 55 | "\r", 56 | "navigator.share({\r", 57 | " title: eventsFunctionContext.getArgument(\"title\") || undefined,\r", 58 | " text: eventsFunctionContext.getArgument(\"text\") || undefined,\r", 59 | " url: eventsFunctionContext.getArgument(\"url\") || undefined,\r", 60 | "})\r", 61 | " .then(() => {\r", 62 | " gdjs._shareExtension.lastShareResult = 'ok';\r", 63 | " eventsFunctionContext.task.resolve();\r", 64 | " })\r", 65 | " .catch(() => {\r", 66 | " gdjs._shareExtension.lastShareResult = 'canceled';\r", 67 | " eventsFunctionContext.task.resolve();\r", 68 | " });\r", 69 | "\r", 70 | "" 71 | ], 72 | "parameterObjects": "", 73 | "useStrict": true, 74 | "eventsSheetExpanded": true 75 | } 76 | ], 77 | "parameters": [ 78 | { 79 | "description": "Text to share", 80 | "name": "text", 81 | "type": "string" 82 | }, 83 | { 84 | "description": "Url to share", 85 | "name": "url", 86 | "type": "string" 87 | }, 88 | { 89 | "description": "Title to show in the Share dialog", 90 | "name": "title", 91 | "type": "string" 92 | } 93 | ], 94 | "objectGroups": [] 95 | }, 96 | { 97 | "description": "Check if the browser/operating system of the device supports sharing. Sharing is typically not supported on desktop browsers or desktop apps.", 98 | "fullName": "Sharing is supported", 99 | "functionType": "Condition", 100 | "name": "IsSharingSupported", 101 | "sentence": "The browser or system supports sharing", 102 | "events": [ 103 | { 104 | "type": "BuiltinCommonInstructions::JsCode", 105 | "inlineCode": "eventsFunctionContext.returnValue = !!navigator.share;", 106 | "parameterObjects": "", 107 | "useStrict": true, 108 | "eventsSheetExpanded": false 109 | } 110 | ], 111 | "parameters": [], 112 | "objectGroups": [] 113 | }, 114 | { 115 | "description": "the result of the last share dialog.", 116 | "fullName": "Result of the last share dialog", 117 | "functionType": "ExpressionAndCondition", 118 | "name": "LastShareResult", 119 | "sentence": "the result of the last share dialog", 120 | "events": [ 121 | { 122 | "type": "BuiltinCommonInstructions::JsCode", 123 | "inlineCode": [ 124 | "gdjs._shareExtension = gdjs._shareExtension || {", 125 | " lastShareResult: '',", 126 | "};", 127 | "", 128 | "eventsFunctionContext.returnValue = gdjs._shareExtension.lastShareResult;" 129 | ], 130 | "parameterObjects": "", 131 | "useStrict": true, 132 | "eventsSheetExpanded": false 133 | } 134 | ], 135 | "expressionType": { 136 | "supplementaryInformation": "[\"unsupported\",\"ok\",\"canceled\"]", 137 | "type": "stringWithSelector" 138 | }, 139 | "parameters": [], 140 | "objectGroups": [] 141 | } 142 | ], 143 | "eventsBasedBehaviors": [], 144 | "eventsBasedObjects": [] 145 | } -------------------------------------------------------------------------------- /extensions/reviewed/SnapToGrid.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "@Lizard-13", 3 | "category": "Game mechanic", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Rectangular grid", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXZpZXctZ3JpZC1wbHVzLW91dGxpbmUiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMyAyMUgxMVYxM0gzTTUgMTVIOVYxOUg1TTMgMTFIMTFWM0gzTTUgNUg5VjlINU0xMyAzVjExSDIxVjNNMTkgOUgxNVY1SDE5TTE4IDE2SDIxVjE4SDE4VjIxSDE2VjE4SDEzVjE2SDE2VjEzSDE4WiIgLz48L3N2Zz4=", 9 | "name": "SnapToGrid", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/view-grid-plus-outline.svg", 11 | "shortDescription": "Snap objects on a virtual grid.", 12 | "version": "0.0.4", 13 | "description": [ 14 | "It allows to snap objects on a *virtual grid*.", 15 | "", 16 | "It's useful for:", 17 | "- level editors", 18 | "- building systems", 19 | "", 20 | "A [simple example](https://editor.gdevelop.io/?project=example://snap-object-to-grid) shows how to snap objects that are dragged with the mouse." 21 | ], 22 | "tags": [ 23 | "snap", 24 | "grid", 25 | "positioning", 26 | "tiles" 27 | ], 28 | "authorIds": [], 29 | "dependencies": [], 30 | "eventsFunctions": [ 31 | { 32 | "description": "Snap object to a virtual grid (i.e: this is not the grid used in the editor).", 33 | "fullName": "Snap objects to a virtual grid", 34 | "functionType": "Action", 35 | "name": "SnapObjectToVirtualGrid", 36 | "sentence": "Snap _PARAM1_ to a virtual grid using cells with width: _PARAM2_px, height _PARAM3_px and an offset position (_PARAM4_; _PARAM5_)", 37 | "events": [ 38 | { 39 | "type": "BuiltinCommonInstructions::Comment", 40 | "color": { 41 | "b": 109, 42 | "g": 230, 43 | "r": 255, 44 | "textB": 0, 45 | "textG": 0, 46 | "textR": 0 47 | }, 48 | "comment": "Round the Object position to snap to the in-game grid", 49 | "comment2": "" 50 | }, 51 | { 52 | "type": "BuiltinCommonInstructions::Standard", 53 | "conditions": [], 54 | "actions": [ 55 | { 56 | "type": { 57 | "value": "MettreXY" 58 | }, 59 | "parameters": [ 60 | "Object", 61 | "=", 62 | "CellWidth * round((Object.X() - OffsetX) / CellWidth) + OffsetX", 63 | "=", 64 | "CellHeight * round((Object.Y() - OffsetY) / CellHeight) + OffsetY" 65 | ] 66 | } 67 | ] 68 | } 69 | ], 70 | "parameters": [ 71 | { 72 | "description": "Objects to snap to the virtual grid", 73 | "name": "Object", 74 | "type": "objectList" 75 | }, 76 | { 77 | "description": "Width of a cell of the virtual grid (in pixels)", 78 | "name": "CellWidth", 79 | "type": "expression" 80 | }, 81 | { 82 | "description": "Height of a cell of the virtual grid (in pixels)", 83 | "name": "CellHeight", 84 | "type": "expression" 85 | }, 86 | { 87 | "description": "Offset on the X axis of the virtual grid (in pixels)", 88 | "name": "OffsetX", 89 | "type": "expression" 90 | }, 91 | { 92 | "description": "Offset on the Y axis of the virtual grid (in pixels)", 93 | "name": "OffsetY", 94 | "type": "expression" 95 | } 96 | ], 97 | "objectGroups": [] 98 | } 99 | ], 100 | "eventsBasedBehaviors": [], 101 | "eventsBasedObjects": [] 102 | } -------------------------------------------------------------------------------- /extensions/reviewed/SpriteMasking.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "category": "Visual effect", 4 | "description": "When masked, the masked object is only visible through the mask.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Object Masking", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXRyYW5zaXRpb24tbWFza2VkIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE1LDJDMTYuOTQsMiAxOC41OSwyLjcgMTkuOTUsNC4wNUMyMS4zLDUuNDEgMjIsNy4wNiAyMiw5QzIyLDEwLjU2IDIxLjUsMTEuOTYgMjAuNTgsMTMuMkMxOS42NCwxNC40MyAxOC40NCwxNS4yNyAxNi45NywxNS43TDE3LDE1LjM4VjE1QzE3LDEyLjgxIDE2LjIzLDEwLjkzIDE0LjY1LDkuMzVDMTMuMDcsNy43NyAxMS4xOSw3IDksN0g4LjYzTDguMyw3LjAzQzguNzMsNS41NiA5LjU3LDQuMzYgMTAuOCwzLjQyQzEyLjA0LDIuNSAxMy40NCwyIDE1LDJNOSw4QTcsNyAwIDAsMSAxNiwxNUE3LDcgMCAwLDEgOSwyMkE3LDcgMCAwLDEgMiwxNUE3LDcgMCAwLDEgOSw4TTksMTBBNSw1IDAgMCwwIDQsMTVBNSw1IDAgMCwwIDksMjBBNSw1IDAgMCwwIDE0LDE1QTUsNSAwIDAsMCA5LDEwWiIgLz48L3N2Zz4=", 10 | "name": "SpriteMasking", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/transition-masked.svg", 12 | "shortDescription": "Use a sprite or a shape painter to mask another object.", 13 | "version": "2.1.1", 14 | "tags": [ 15 | "masking", 16 | "javascript", 17 | "pixi", 18 | "sprites" 19 | ], 20 | "authorIds": [ 21 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 22 | ], 23 | "dependencies": [], 24 | "eventsFunctions": [ 25 | { 26 | "description": "Define a shape painter as a mask of an object.", 27 | "fullName": "Mask an object with a shape painter", 28 | "functionType": "Action", 29 | "name": "MaskWithShapePainter", 30 | "private": false, 31 | "sentence": "Mask _PARAM1_ with mask _PARAM2_", 32 | "events": [ 33 | { 34 | "disabled": false, 35 | "folded": false, 36 | "type": "BuiltinCommonInstructions::JsCode", 37 | "inlineCode": "const maskObject = eventsFunctionContext.getObjects(\"Mask\")[0];\nif (!maskObject) return;\n\nconst maskedObjects = eventsFunctionContext.getObjects(\"Masked\");\nfor (const maskedObject of maskedObjects) {\n const maskedRenderer = maskedObject.getRendererObject(); \n maskedRenderer.mask = maskObject.getRendererObject();\n}\n\n", 38 | "parameterObjects": "masked", 39 | "useStrict": true, 40 | "eventsSheetExpanded": false 41 | } 42 | ], 43 | "parameters": [ 44 | { 45 | "codeOnly": false, 46 | "defaultValue": "", 47 | "description": "Object to mask", 48 | "longDescription": "", 49 | "name": "Masked", 50 | "optional": false, 51 | "supplementaryInformation": "", 52 | "type": "objectList" 53 | }, 54 | { 55 | "codeOnly": false, 56 | "defaultValue": "", 57 | "description": "Shape painter to use as a mask", 58 | "longDescription": "", 59 | "name": "Mask", 60 | "optional": false, 61 | "supplementaryInformation": "PrimitiveDrawing::Drawer", 62 | "type": "objectList" 63 | } 64 | ], 65 | "objectGroups": [] 66 | }, 67 | { 68 | "description": "Define a sprite as a mask of an object.", 69 | "fullName": "Mask an object with a sprite", 70 | "functionType": "Action", 71 | "name": "Mask", 72 | "private": false, 73 | "sentence": "Mask _PARAM1_ with mask _PARAM2_", 74 | "events": [ 75 | { 76 | "disabled": false, 77 | "folded": false, 78 | "type": "BuiltinCommonInstructions::JsCode", 79 | "inlineCode": "const maskObject = eventsFunctionContext.getObjects(\"Mask\")[0];\nif (!maskObject) return;\n\nconst maskedObjects = eventsFunctionContext.getObjects(\"Masked\");\nfor (const maskedObject of maskedObjects) {\n const maskedRenderer = maskedObject.getRendererObject(); \n maskedRenderer.mask = maskObject.getRendererObject();\n}\n\n", 80 | "parameterObjects": "masked", 81 | "useStrict": true, 82 | "eventsSheetExpanded": false 83 | } 84 | ], 85 | "parameters": [ 86 | { 87 | "codeOnly": false, 88 | "defaultValue": "", 89 | "description": "Object to mask", 90 | "longDescription": "", 91 | "name": "Masked", 92 | "optional": false, 93 | "supplementaryInformation": "", 94 | "type": "objectList" 95 | }, 96 | { 97 | "codeOnly": false, 98 | "defaultValue": "", 99 | "description": "Sprite object to use as a mask", 100 | "longDescription": "", 101 | "name": "Mask", 102 | "optional": false, 103 | "supplementaryInformation": "Sprite", 104 | "type": "objectList" 105 | } 106 | ], 107 | "objectGroups": [] 108 | }, 109 | { 110 | "description": "Remove the mask of the specified object.", 111 | "fullName": "Remove the mask", 112 | "functionType": "Action", 113 | "name": "Unmask", 114 | "private": false, 115 | "sentence": "Remove the mask of _PARAM1_", 116 | "events": [ 117 | { 118 | "disabled": false, 119 | "folded": false, 120 | "type": "BuiltinCommonInstructions::JsCode", 121 | "inlineCode": "const maskedObjects = eventsFunctionContext.getObjects(\"Masked\");\n\nfor (const maskedObject of maskedObjects) {\n const maskedRenderer = maskedObject.getRendererObject(); \n maskedRenderer.mask = null;\n}\n", 122 | "parameterObjects": "", 123 | "useStrict": true, 124 | "eventsSheetExpanded": false 125 | } 126 | ], 127 | "parameters": [ 128 | { 129 | "codeOnly": false, 130 | "defaultValue": "", 131 | "description": "Object with a mask to remove", 132 | "longDescription": "", 133 | "name": "Masked", 134 | "optional": false, 135 | "supplementaryInformation": "", 136 | "type": "objectList" 137 | } 138 | ], 139 | "objectGroups": [] 140 | } 141 | ], 142 | "eventsBasedBehaviors": [] 143 | } -------------------------------------------------------------------------------- /extensions/reviewed/TimeFormatter.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "@Bouh", 3 | "category": "User interface", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "Time formatting", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWNsb2NrLWRpZ2l0YWwiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMiw2QTIsMiAwIDAsMCAwLDhWMTZBMiwyIDAgMCwwIDIsMThIMjJBMiwyIDAgMCwwIDI0LDE2VjhBMiwyIDAgMCwwIDIyLDZNMiw4SDIyVjE2SDJNMyw5VjEwLjVINi4yNUwzLDE1SDQuNzVMOCwxMC41VjlNOS4yNSw5VjEwLjVIMTAuNzVWOU0xMiw5VjEwLjVIMTMuNVYxNUgxNVY5TTE3LDlBMSwxIDAgMCwwIDE2LDEwVjE0QTEsMSAwIDAsMCAxNywxNUgyMEExLDEgMCAwLDAgMjEsMTRWMTBBMSwxIDAgMCwwIDIwLDlNMTcuNSwxMC41SDE5LjVWMTMuNUgxNy41TTkuMjUsMTMuNVYxNUgxMC43NVYxMy41IiAvPjwvc3ZnPg==", 9 | "name": "TimeFormatter", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/clock-digital.svg", 11 | "shortDescription": "Converts seconds into standard time formats, such as HH:MM:SS. ", 12 | "version": "0.0.2", 13 | "description": [ 14 | "Ideal for displaying timers on screen.", 15 | "", 16 | "Formats included:", 17 | "* HH:MM:SS", 18 | "* HH:MM:SS.000 (displays milliseconds)" 19 | ], 20 | "origin": { 21 | "identifier": "TimeFormatter", 22 | "name": "gdevelop-extension-store" 23 | }, 24 | "tags": [ 25 | "time", 26 | "timer", 27 | "format", 28 | "hours", 29 | "minutes", 30 | "seconds", 31 | "milliseconds" 32 | ], 33 | "authorIds": [ 34 | "2OwwM8ToR9dx9RJ2sAKTcrLmCB92" 35 | ], 36 | "dependencies": [], 37 | "eventsFunctions": [ 38 | { 39 | "description": "Format time in seconds to HH:MM:SS.", 40 | "fullName": "Format time in seconds to HH:MM:SS", 41 | "functionType": "StringExpression", 42 | "name": "SecondsToHHMMSS", 43 | "sentence": "Format time _PARAM1_ to HH:MM:SS in _PARAM2_", 44 | "events": [ 45 | { 46 | "type": "BuiltinCommonInstructions::JsCode", 47 | "inlineCode": [ 48 | "var format_time = function (time_second) {\r", 49 | " date = new Date(null);\r", 50 | " date.setSeconds(time_second);\r", 51 | " if (time_second >= 3600) {\r", 52 | " return date.toISOString().substr(11, 8); // MM:SS\r", 53 | " } else {\r", 54 | " return date.toISOString().substr(14, 5); // HH:MM:SS\r", 55 | " }\r", 56 | "}\r", 57 | "\r", 58 | "eventsFunctionContext.returnValue = format_time(eventsFunctionContext.getArgument(\"TimeInSeconds\"));" 59 | ], 60 | "parameterObjects": "", 61 | "useStrict": false, 62 | "eventsSheetExpanded": false 63 | } 64 | ], 65 | "expressionType": { 66 | "type": "string" 67 | }, 68 | "parameters": [ 69 | { 70 | "description": "Time, in seconds", 71 | "name": "TimeInSeconds", 72 | "type": "expression" 73 | } 74 | ], 75 | "objectGroups": [] 76 | }, 77 | { 78 | "description": "Format time in seconds to HH:MM:SS.000, including milliseconds.", 79 | "fullName": "Format time in seconds to HH:MM:SS.000", 80 | "functionType": "StringExpression", 81 | "name": "SecondsToHHMMSS000", 82 | "sentence": "Format time _PARAM1_ to HH:MM:SS in _PARAM2_", 83 | "events": [ 84 | { 85 | "type": "BuiltinCommonInstructions::JsCode", 86 | "inlineCode": [ 87 | "var format_time = function (time_second) {\r", 88 | " date = new Date(null);\r", 89 | " date.setMilliseconds(1000*time_second);\r", 90 | " if (time_second >= 3600) {\r", 91 | " return date.toISOString().substr(11, 12); // MM:SS.000\r", 92 | " } else {\r", 93 | " return date.toISOString().substr(14, 9); // HH:MM:SS.000\r", 94 | " }\r", 95 | "}\r", 96 | "\r", 97 | "eventsFunctionContext.returnValue = format_time(eventsFunctionContext.getArgument(\"TimeInSeconds\"));" 98 | ], 99 | "parameterObjects": "", 100 | "useStrict": false, 101 | "eventsSheetExpanded": false 102 | } 103 | ], 104 | "expressionType": { 105 | "type": "string" 106 | }, 107 | "parameters": [ 108 | { 109 | "description": "Time, in seconds", 110 | "name": "TimeInSeconds", 111 | "type": "expression" 112 | } 113 | ], 114 | "objectGroups": [] 115 | } 116 | ], 117 | "eventsBasedBehaviors": [], 118 | "eventsBasedObjects": [] 119 | } -------------------------------------------------------------------------------- /extensions/reviewed/UUID.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Arthur Pacaud (arthuro555)", 3 | "category": "Advanced", 4 | "description": "Adds UUID (Universally Unique Identifiers) generation expressions via multiple patterns:\n- UUIDv4: Creates a long random string of characters following the UUIDv4 standard. If available on the system/browser, will use a cryptographic random number generator, otherwise uses the same pseudorandom number generator as the `Random()` expression. Chances of collisions are extremely low, but not non-existent. As the return value is a string, it may not be the most performant pattern. It can not be predicted in most cases.\n- Incremented integer: Returns use an integer that will be incremented after each call. Very performant and no risk of collisions. The UID will be predictable, so it may be vulnerable to some cryptographic attacks if used for private unique tokens, like password reset verification UID. Note that if you store IDs and then restart the game, there may be duplicates, since it'll reset the counter.", 5 | "extensionNamespace": "", 6 | "gdevelopVersion": ">=5.5.222", 7 | "fullName": "Unique Identifiers", 8 | "helpPath": "", 9 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWlkZW50aWZpZXIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNiwxOFY4SDhWMThINk02LDQuNUg4VjYuNUg2VjQuNU0xNyw0SDE5VjE4SDE3VjE3Ljc1QzE3LDE3Ljc1IDE1LjY3LDE4IDE1LDE4QTUsNSAwIDAsMSAxMCwxM0E1LDUgMCAwLDEgMTUsOEMxNS42Nyw4IDE3LDguMjUgMTcsOC4yNVY0TTE3LDEwLjI1QzE3LDEwLjI1IDE1LjY3LDEwIDE1LDEwQTMsMyAwIDAsMCAxMiwxM0EzLDMgMCAwLDAgMTUsMTZDMTUuNjcsMTYgMTcsMTUuNzUgMTcsMTUuNzVWMTAuMjVaIiAvPjwvc3ZnPg==", 10 | "name": "UUID", 11 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/identifier.svg", 12 | "shortDescription": "A collection of UID generation expressions.", 13 | "version": "1.0.0", 14 | "tags": [ 15 | "random", 16 | "generation", 17 | "uid", 18 | "uuid", 19 | "guid", 20 | "v4", 21 | "unique", 22 | "id", 23 | "identifier" 24 | ], 25 | "authorIds": [ 26 | "ZgrsWuRTAkXgeuPV9bo0zuEcA2w1" 27 | ], 28 | "dependencies": [], 29 | "eventsFunctions": [ 30 | { 31 | "description": "Generates a unique identifier with the UUIDv4 pattern.", 32 | "fullName": "Generate a UUIDv4", 33 | "functionType": "StringExpression", 34 | "name": "GenerateUUIDv4", 35 | "private": false, 36 | "sentence": "", 37 | "events": [ 38 | { 39 | "disabled": false, 40 | "folded": false, 41 | "type": "BuiltinCommonInstructions::JsCode", 42 | "inlineCode": "// Use the engine implementation of UUIDv4.\neventsFunctionContext.returnValue = gdjs.makeUuid();\n", 43 | "parameterObjects": "", 44 | "useStrict": true, 45 | "eventsSheetExpanded": false 46 | } 47 | ], 48 | "parameters": [], 49 | "objectGroups": [] 50 | }, 51 | { 52 | "description": "Generates a unique identifier with the incremented integer pattern.", 53 | "fullName": "Generate an incremented integer UID", 54 | "functionType": "Expression", 55 | "name": "GenerateIncrementedIntegerUID", 56 | "private": false, 57 | "sentence": "", 58 | "events": [ 59 | { 60 | "disabled": false, 61 | "folded": false, 62 | "type": "BuiltinCommonInstructions::Standard", 63 | "conditions": [], 64 | "actions": [ 65 | { 66 | "type": { 67 | "inverted": false, 68 | "value": "SetReturnNumber" 69 | }, 70 | "parameters": [ 71 | "GlobalVariable(__UUID_IncrementedInteger)" 72 | ], 73 | "subInstructions": [] 74 | }, 75 | { 76 | "type": { 77 | "inverted": false, 78 | "value": "ModVarGlobal" 79 | }, 80 | "parameters": [ 81 | "__UUID_IncrementedInteger", 82 | "+", 83 | "1" 84 | ], 85 | "subInstructions": [] 86 | } 87 | ], 88 | "events": [] 89 | } 90 | ], 91 | "parameters": [], 92 | "objectGroups": [] 93 | } 94 | ], 95 | "eventsBasedBehaviors": [] 96 | } -------------------------------------------------------------------------------- /extensions/reviewed/YSort.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gustavo Marciano", 3 | "category": "Visual effect", 4 | "extensionNamespace": "", 5 | "gdevelopVersion": ">=5.5.222", 6 | "fullName": "YSort", 7 | "helpPath": "", 8 | "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLXNvcnQtYXNjZW5kaW5nIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE5IDE3SDIyTDE4IDIxTDE0IDE3SDE3VjNIMTlNMiAxN0gxMlYxOUgyTTYgNVY3SDJWNU0yIDExSDlWMTNIMlYxMVoiIC8+PC9zdmc+", 9 | "name": "YSort", 10 | "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/sort-ascending.svg", 11 | "shortDescription": "Create an illusion of depth by setting the Z-order based on the Y position of the object. Useful for isometric games, 2D games with a \"Top-Down\" view, RPG...", 12 | "version": "0.1.0", 13 | "description": [ 14 | "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.", 15 | "", 16 | "This is useful for:", 17 | "- isometric games ([open the project online](https://editor.gdevelop.io/?project=example://isometric-game))", 18 | "- top-down games ([open the project online](https://editor.gdevelop.io/?project=example://top-down-grid-movement))" 19 | ], 20 | "tags": [ 21 | "z-order", 22 | "y-sort", 23 | "depth", 24 | "fake-depth", 25 | "isometric", 26 | "rpg" 27 | ], 28 | "authorIds": [], 29 | "dependencies": [], 30 | "eventsFunctions": [], 31 | "eventsBasedBehaviors": [ 32 | { 33 | "description": "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.", 34 | "fullName": "YSort", 35 | "name": "YSort", 36 | "objectType": "", 37 | "eventsFunctions": [ 38 | { 39 | "fullName": "", 40 | "functionType": "Action", 41 | "name": "doStepPostEvents", 42 | "sentence": "", 43 | "events": [ 44 | { 45 | "type": "BuiltinCommonInstructions::Comment", 46 | "color": { 47 | "b": 109, 48 | "g": 230, 49 | "r": 255, 50 | "textB": 0, 51 | "textG": 0, 52 | "textR": 0 53 | }, 54 | "comment": "This is done in doStepPostEvents because the sort must happens right before the rendering to avoid a 1 frame delay.", 55 | "comment2": "" 56 | }, 57 | { 58 | "type": "BuiltinCommonInstructions::Standard", 59 | "conditions": [], 60 | "actions": [ 61 | { 62 | "type": { 63 | "value": "ChangePlan" 64 | }, 65 | "parameters": [ 66 | "Object", 67 | "=", 68 | "Object.Y()" 69 | ] 70 | } 71 | ] 72 | } 73 | ], 74 | "parameters": [ 75 | { 76 | "description": "Object", 77 | "name": "Object", 78 | "type": "object" 79 | }, 80 | { 81 | "description": "Behavior", 82 | "name": "Behavior", 83 | "supplementaryInformation": "YSort::YSort", 84 | "type": "behavior" 85 | } 86 | ], 87 | "objectGroups": [] 88 | } 89 | ], 90 | "propertyDescriptors": [], 91 | "sharedPropertyDescriptors": [] 92 | } 93 | ], 94 | "eventsBasedObjects": [] 95 | } -------------------------------------------------------------------------------- /extensions/views.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "firstExtensionIds": ["Gamepads", "CameraShake", "Health", "FireBullet", "SmoothCamera", "Sticker"], 4 | "firstBehaviorIds": [ 5 | { "extensionName": "Health", "behaviorName": "Health" }, 6 | { "extensionName": "FireBullet", "behaviorName": "FireBullet" }, 7 | { "extensionName": "SmoothCamera", "behaviorName": "SmoothCamera" }, 8 | { "extensionName": "Sticker", "behaviorName": "Sticker" } 9 | ], 10 | "firstObjectIds": [ 11 | { "extensionName": "PanelSpriteButton", "objectName": "PanelSpriteButton" }, 12 | { "extensionName": "SpriteMultitouchJoystick", "objectName": "SpriteMultitouchJoystick" }, 13 | { "extensionName": "PanelSpriteContinuousBar", "objectName": "PanelSpriteContinuousBar" }, 14 | { "extensionName": "TiledUnitsBar", "objectName": "TiledUnitsBar" }, 15 | { "extensionName": "PanelSpriteSlider", "objectName": "PanelSpriteSlider" }, 16 | { "extensionName": "SpriteToggleSwitch", "objectName": "SpriteToggleSwitch" } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gdevelop-extensions", 3 | "version": "1.0.0", 4 | "description": "Repository of extensions for GDevelop", 5 | "scripts": { 6 | "test": "jest scripts/", 7 | "format": "prettier --write \"scripts/**/*.ts\" \"scripts/**/*.js\"", 8 | "build": "node scripts/generate-extensions-registry.js", 9 | "deploy": "node scripts/deploy.js", 10 | "check-post-build": "jest post-build", 11 | "check-format": "prettier --list-different \"scripts/**/*.ts\" \"scripts/**/*.js\"", 12 | "check-types": "tsc", 13 | "extract-all-translations": "node scripts/extract-all-translations.js", 14 | "compile-translations": "node scripts/compile-translations.js" 15 | }, 16 | "author": "Florian Rival <Florian.Rival@gmail.com>", 17 | "license": "MIT", 18 | "dependencies": { 19 | "axios": "^0.21.1", 20 | "jszip": "^3.10.0", 21 | "minimist": "^1.2.5", 22 | "semver": "^7.3.5", 23 | "shelljs": "^0.8.4" 24 | }, 25 | "devDependencies": { 26 | "@lingui/cli": "2.7.3", 27 | "@types/jest": "^26.0.23", 28 | "@types/jszip": "^3.4.1", 29 | "@types/minimist": "^1.2.1", 30 | "@types/semver": "^7.3.8", 31 | "@types/shelljs": "^0.8.8", 32 | "iso-639-1": "3.1.2", 33 | "jest": "^27.0.1", 34 | "prettier": "^2.3.0", 35 | "typescript": "^4.3.2" 36 | }, 37 | "jest": { 38 | "testPathIgnorePatterns": [ 39 | "/node_modules/", 40 | "<rootDir>/dist/" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/__tests__/no-test-for-now.spec.js: -------------------------------------------------------------------------------- 1 | describe('No unit tests for now', () => { 2 | // But we have post build tests. 3 | test('Add some when needed', () => { 4 | expect(true).toBe(true); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /scripts/check-single-extension.js: -------------------------------------------------------------------------------- 1 | const { readFile } = require('fs/promises'); 2 | const { isValidExtensionName } = require('./lib/ExtensionNameValidator'); 3 | const { validateExtension } = require('./lib/ExtensionValidator'); 4 | 5 | /** 6 | * A function used by the CI to check for issues in a single extension. 7 | * @param {string} extensionName 8 | * @param {{extensionsFolder?: string, preliminaryCheck?: boolean}} [options] 9 | * @returns {Promise<{code: "invalid-file-name" | "not-found" | "duplicated" | "invalid-json" | "unknown-json-contents" | "gdevelop-project-file" | "success"} | {code: "rule-break", errors: string[]}>} 10 | */ 11 | exports.verifyExtension = async function (extensionName, options) { 12 | const { 13 | extensionsFolder = `${__dirname}/../extensions`, 14 | preliminaryCheck = false, 15 | } = options || {}; 16 | // Make sure the name is valid, as dots are not allowed in the name 17 | // and could be used to do relative path shenanigans that could result in skipping automatic checks. 18 | if (!isValidExtensionName(extensionName)) 19 | return { code: 'invalid-file-name' }; 20 | 21 | const [community, reviewed] = await Promise.all([ 22 | readFile(`${extensionsFolder}/community/${extensionName}.json`).catch( 23 | () => null 24 | ), 25 | readFile(`${extensionsFolder}/reviewed/${extensionName}.json`).catch( 26 | () => null 27 | ), 28 | ]); 29 | 30 | if (!community && !reviewed) return { code: 'not-found' }; 31 | if (community && reviewed) return { code: 'duplicated' }; 32 | 33 | //@ts-ignore We know this cannot be null thanks to the checks done just before 34 | /** @type {string} */ const file = ( 35 | community ? community : reviewed 36 | ).toString(); 37 | 38 | /** @type {any} */ 39 | let extension; 40 | try { 41 | extension = JSON.parse(file); 42 | } catch { 43 | return { code: 'invalid-json' }; 44 | } 45 | 46 | // Basic check to see if it is a GDevelop project 47 | if ( 48 | typeof extension.properties === 'object' && 49 | typeof extension.properties.name === 'string' 50 | ) { 51 | return { code: 'gdevelop-project-file' }; 52 | } 53 | 54 | // Basic check to see if it is a GDevelop extension 55 | if ( 56 | !( 57 | Array.isArray(extension.eventsFunctions) && 58 | Array.isArray(extension.eventsBasedBehaviors) && 59 | typeof extension.name === 'string' 60 | ) 61 | ) { 62 | return { code: 'unknown-json-contents' }; 63 | } 64 | 65 | const validationDetails = await validateExtension( 66 | { 67 | state: 'success', 68 | extension, 69 | tier: community ? 'community' : 'reviewed', 70 | filename: `${extensionName}.json`, 71 | }, 72 | preliminaryCheck 73 | ); 74 | 75 | if (validationDetails.length > 0) 76 | return { 77 | code: 'rule-break', 78 | errors: validationDetails.map(({ message }) => message), 79 | }; 80 | 81 | return { code: 'success' }; 82 | }; 83 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const path = require('path'); 3 | const { default: axios } = require('axios'); 4 | const args = require('minimist')(process.argv.slice(2)); 5 | 6 | const databasePath = path.join(__dirname, '../dist/extensions-database'); 7 | const extensionsPath = path.join(__dirname, '../dist/extensions'); 8 | const databaseDestination = `s3://resources.gdevelop-app.com/extensions-database`; 9 | const extensionsDestination = `s3://resources.gdevelop-app.com/extensions`; 10 | 11 | if (!args['cf-zoneid'] || !args['cf-token']) { 12 | shell.echo( 13 | '❌ You must pass --cf-zoneid, --cf-token to purge the CloudFare cache.' 14 | ); 15 | shell.exit(1); 16 | } 17 | 18 | { 19 | shell.echo( 20 | 'ℹ️ Uploading extensions to resources.gdevelop-app.com/extensions...' 21 | ); 22 | const output = shell.exec( 23 | `aws s3 sync ${extensionsPath} ${extensionsDestination} --acl public-read` 24 | ); 25 | if (output.code !== 0) { 26 | shell.echo( 27 | '❌ Unable to upload database to resources.gdevelop-app.com/extensions-database. Error is:' 28 | ); 29 | shell.echo(output.stdout); 30 | shell.echo(output.stderr); 31 | shell.exit(output.code); 32 | } 33 | } 34 | 35 | { 36 | shell.echo( 37 | 'ℹ️ Uploading database to resources.gdevelop-app.com/extensions-database...' 38 | ); 39 | const output = shell.exec( 40 | `aws s3 sync ${databasePath} ${databaseDestination} --acl public-read` 41 | ); 42 | if (output.code !== 0) { 43 | shell.echo( 44 | '❌ Unable to upload database to resources.gdevelop-app.com/extensions-database. Error is:' 45 | ); 46 | shell.echo(output.stdout); 47 | shell.echo(output.stderr); 48 | shell.exit(output.code); 49 | } 50 | } 51 | 52 | shell.echo('✅ Upload finished'); 53 | 54 | shell.echo('ℹ️ Purging Cloudflare cache...'); 55 | 56 | const zoneId = args['cf-zoneid']; 57 | const purgeCacheUrl = `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`; 58 | 59 | axios 60 | .post( 61 | purgeCacheUrl, 62 | { 63 | files: [ 64 | // Update the "database". 65 | 'https://resources.gdevelop-app.com/extensions-database/extensions-database.json', 66 | // Also purge the query params that can be used by backend services: 67 | 'https://resources.gdevelop-app.com/extensions-database/extensions-database.json?cache-version=1', 68 | 'https://resources.gdevelop-app.com/extensions-database/extensions-database.json?cache-version=2', 69 | ], 70 | }, 71 | { 72 | headers: { 73 | Authorization: `Bearer ${args['cf-token']}`, 74 | 'Content-Type': 'application/json', 75 | }, 76 | } 77 | ) 78 | .then((response) => response.data) 79 | .then(() => { 80 | shell.echo('✅ Cache purge done.'); 81 | }) 82 | .catch((error) => { 83 | shell.echo( 84 | '❌ Error while requesting cache purge (are your identifiers correct?)' 85 | ); 86 | shell.echo(error.message || '(unknown error)'); 87 | shell.exit(1); 88 | }); 89 | -------------------------------------------------------------------------------- /scripts/extract-extension.js: -------------------------------------------------------------------------------- 1 | const JSZip = require('jszip'); 2 | const { isValidExtensionName } = require('./lib/ExtensionNameValidator'); 3 | const { createWriteStream } = require('fs'); 4 | const { parse: parsePath } = require('path'); 5 | const { readFile } = require('fs/promises'); 6 | const pipeline = require('util').promisify(require('stream').pipeline); 7 | 8 | /** 9 | * Extracts exactly one extension into the community extensions folder from a zip file. 10 | * @param {string} zipPath The path to the zip file to extract. 11 | * @param {string} [extensionsFolder] The folder with the extensions. 12 | * @returns {Promise<{error: "too-many-files" | "no-json-found"| "invalid-file-name" | "zip-error", details?: any} | {error?: undefined,extensionName: string}>} the name of the extracted extension if successful, else a generic error code. 13 | */ 14 | exports.extractExtension = async function ( 15 | zipPath, 16 | extensionsFolder = `${__dirname}/../extensions` 17 | ) { 18 | // Load in the archive with JSZip 19 | const zip = await JSZip.loadAsync(await readFile(zipPath)).catch((e) => { 20 | console.warn(`JSZip loading error caught: `, e); 21 | return null; 22 | }); 23 | if (zip === null) return { error: 'zip-error' }; 24 | 25 | // Find JSON files in the zip top level folder 26 | const jsonFiles = zip.file(/.*\.json$/); 27 | 28 | // Ensure there is exactly 1 file 29 | if (jsonFiles.length === 0) return { error: 'no-json-found' }; 30 | if (jsonFiles.length > 1) return { error: 'too-many-files' }; 31 | const [file] = jsonFiles; 32 | 33 | const { name: extensionName, dir, ext } = parsePath(file.name); 34 | if (ext !== '.json') return { error: 'invalid-file-name' }; 35 | 36 | // Ensure no special characters are in the extension name to prevent relative path 37 | // name shenanigans with dots that could put the extension in the reviewed folder. 38 | if (dir) return { error: 'invalid-file-name' }; 39 | if (!isValidExtensionName(extensionName)) 40 | return { error: 'invalid-file-name' }; 41 | 42 | try { 43 | // Write the extension to the community extensions folder 44 | await pipeline( 45 | file.nodeStream(), 46 | createWriteStream(`${extensionsFolder}/community/${file.name}`) 47 | ); 48 | } catch (e) { 49 | console.warn(`JSZip extraction error caught: `, e); 50 | return { error: 'zip-error' }; 51 | } 52 | 53 | return { extensionName }; 54 | }; 55 | -------------------------------------------------------------------------------- /scripts/lib.es5.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fixes https://github.com/microsoft/TypeScript/issues/16655 for `Array.prototype.filter()` 3 | * For example, using the fix the type of `bar` is `string[]` in the below snippet as it should be. 4 | * 5 | * const foo: (string | null | undefined)[] = []; 6 | * const bar = foo.filter(Boolean); 7 | * 8 | * For related definitions, see https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts 9 | * 10 | * Original licenses apply, see 11 | * - https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt 12 | * - https://stackoverflow.com/help/licensing 13 | */ 14 | 15 | /** See https://stackoverflow.com/a/51390763/1470607 */ 16 | type Falsy = false | 0 | '' | null | undefined; 17 | 18 | interface Array<T> { 19 | /** 20 | * Returns the elements of an array that meet the condition specified in a callback function. 21 | * @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. 22 | * @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. 23 | */ 24 | filter<S extends T>( 25 | predicate: BooleanConstructor, 26 | thisArg?: any 27 | ): Exclude<S, Falsy>[]; 28 | } 29 | -------------------------------------------------------------------------------- /scripts/lib/ExtensionNameValidator.js: -------------------------------------------------------------------------------- 1 | /** @param {string} extensionName */ 2 | exports.isValidExtensionName = (extensionName) => { 3 | if (extensionName.length === 0) return false; 4 | 5 | // Ensure that the first character is an uppercase character 6 | const firstCharCode = extensionName.charCodeAt(0); 7 | if (firstCharCode < 65 || firstCharCode > 90) return false; 8 | 9 | const len = extensionName.length; 10 | for (let i = 1; i < len; i++) { 11 | const charCode = extensionName.charCodeAt(i); 12 | // Use the ascii table to check quickly if a character is a normal upper- or lowercase character or a number, 13 | // as only those are allowed as extension names. 14 | if ( 15 | // If below numbers range... 16 | charCode < 48 || 17 | // ...between numbers and uppercase letters range... 18 | (charCode > 57 && charCode < 65) || 19 | // ...between uppercase letters and lowercase letters range... 20 | (charCode > 90 && charCode < 97) || 21 | // ...or after the lowercase letters range... 22 | charCode > 122 23 | ) 24 | // ...then it is not in an authorized range. 25 | return false; 26 | } 27 | 28 | return true; 29 | }; 30 | -------------------------------------------------------------------------------- /scripts/lib/ExtensionValidator.js: -------------------------------------------------------------------------------- 1 | const { lifecycleFunctions } = require('./ExtensionsValidatorExceptions'); 2 | const { readdir } = require('fs').promises; 3 | const { join, extname } = require('path'); 4 | 5 | /** @typedef {import("../types").ExtensionWithProperFileInfo} ExtensionWithProperFileInfo */ 6 | /** @typedef {import("../types").ExtensionWithFileInfo} ExtensionWithFileInfo */ 7 | /** @typedef {import("../types").EventsFunction} EventsFunction */ 8 | /** @typedef {import("../types").Error} Error */ 9 | /** @typedef {import("./rules/rule").RuleModule} RuleModule */ 10 | 11 | const rulesPath = join(__dirname, 'rules'); 12 | /** @type {RuleModule[]} */ 13 | const rules = []; 14 | const loadRules = (async function loadRules() { 15 | for (const ruleFile of await readdir(rulesPath)) { 16 | if (extname(ruleFile) !== '.js' || ruleFile === '_Template.js') continue; 17 | /** @type {import("./rules/rule").RuleModule} */ 18 | const rule = require(join(rulesPath, ruleFile)); 19 | rules.push(rule); 20 | } 21 | })(); 22 | 23 | /** 24 | * Check the extension for any properties that are not on the allow list. 25 | * @param {ExtensionWithProperFileInfo} extensionWithFileInfo 26 | * @param {boolean} preliminaryCheck True if we are to skip some checks meant for the reviewer, not the extension creator. 27 | * @returns {Promise<Error[]>} 28 | */ 29 | async function validateExtension( 30 | extensionWithFileInfo, 31 | preliminaryCheck = false 32 | ) { 33 | /** @type {Error[]} */ 34 | const errors = []; 35 | const { eventsBasedBehaviors, eventsBasedObjects, eventsFunctions } = 36 | extensionWithFileInfo.extension; 37 | 38 | const behaviorFunctions = eventsBasedBehaviors.flatMap( 39 | ({ eventsFunctions }) => eventsFunctions 40 | ); 41 | const objectFunctions = eventsBasedObjects 42 | ? eventsBasedObjects.flatMap(({ eventsFunctions }) => eventsFunctions) 43 | : []; 44 | /** 45 | * A list of all events functions of the extension. 46 | * @type {EventsFunction[]} 47 | */ 48 | const allEventsFunctions = []; 49 | Array.prototype.push.apply(allEventsFunctions, eventsFunctions); 50 | Array.prototype.push.apply(allEventsFunctions, behaviorFunctions); 51 | Array.prototype.push.apply(allEventsFunctions, objectFunctions); 52 | 53 | /** 54 | * A list of all events functions that will be used by extension users (non-lifecycle and non-private functions). 55 | * @type {EventsFunction[]} 56 | */ 57 | const publicEventsFunctions = allEventsFunctions.filter( 58 | ({ name, private: p }) => !lifecycleFunctions.has(name) && !p 59 | ); 60 | 61 | // Ensure the rules are loaded before starting verification 62 | if (rules.length === 0) await loadRules; 63 | 64 | const promises = []; 65 | for (const rule of rules) { 66 | if (preliminaryCheck && rule.ignoreDuringPreliminaryChecks) continue; 67 | promises.push( 68 | rule.validate({ 69 | allEventsFunctions, 70 | publicEventsFunctions, 71 | onError: (message, fix) => 72 | errors.push({ 73 | message: `[${rule.name}]: ${message}`, 74 | fix, 75 | }), 76 | ...extensionWithFileInfo, 77 | }) 78 | ); 79 | } 80 | 81 | await Promise.all(promises); 82 | 83 | return errors; 84 | } 85 | 86 | /** 87 | * Check there are no duplicates in extension names. 88 | * @param {ExtensionWithFileInfo[]} extensionWithFileInfos 89 | * @returns {Error[]} 90 | */ 91 | function validateNoDuplicates(extensionWithFileInfos) { 92 | /** @type {Error[]} */ 93 | const errors = []; 94 | 95 | /** @type {Set<string>} */ 96 | const nameAlreadyFound = new Set(); 97 | extensionWithFileInfos.forEach((extensionWithFileInfo) => { 98 | if (extensionWithFileInfo.state === 'success') { 99 | const { name } = extensionWithFileInfo.extension; 100 | if (nameAlreadyFound.has(name)) { 101 | errors.push({ 102 | message: `[${name}]: There are multiple extensions using this name.`, 103 | }); 104 | } else { 105 | nameAlreadyFound.add(name); 106 | } 107 | } 108 | }); 109 | 110 | return errors; 111 | } 112 | 113 | module.exports = { 114 | loadRules, 115 | validateNoDuplicates, 116 | validateExtension, 117 | }; 118 | -------------------------------------------------------------------------------- /scripts/lib/Locales.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const ISO6391 = require('iso-639-1'); 4 | 5 | const rootPath = path.join(__dirname, '../..'); 6 | const translationsPath = path.join(rootPath, '.translations'); 7 | 8 | /** 9 | * @param {string} langLongCode 10 | * @returns {string} 11 | */ 12 | const getShortestCode = (langLongCode) => { 13 | if (langLongCode === 'pt_BR') return langLongCode; 14 | 15 | const langParts = langLongCode.split('_'); 16 | return langParts[0]; 17 | }; 18 | 19 | /** 20 | * @returns {Promise<string[]>} 21 | */ 22 | const getLocales = () => { 23 | return new Promise((resolve, reject) => { 24 | fs.readdir(translationsPath, (error, locales) => { 25 | if (error) { 26 | return reject(error); 27 | } 28 | 29 | return resolve( 30 | locales 31 | .filter((name) => name !== '.DS_Store') 32 | .filter((name) => name !== 'LocalesMetadata.js') 33 | .filter((name) => name !== '_build') 34 | // ensure it's a directory 35 | .filter((name) => { 36 | const fullPath = path.join(translationsPath, name); 37 | return fs.statSync(fullPath).isDirectory(); 38 | }) 39 | ); 40 | }); 41 | }); 42 | }; 43 | 44 | /** 45 | * @param {string} localeName 46 | * @returns {string[]} 47 | */ 48 | const getLocaleSourceCatalogFiles = (localeName) => { 49 | if (localeName === 'en') return ['reviewed-extensions-messages.pot']; 50 | return ['reviewed-extensions-messages.po']; 51 | }; 52 | 53 | /** 54 | * @param {string} localeName 55 | * @returns {string} 56 | */ 57 | const getLocalePath = (localeName) => { 58 | return path.join(translationsPath, localeName); 59 | }; 60 | 61 | /** 62 | * @param {string} localeName 63 | * @returns {string} 64 | */ 65 | const getLocaleCatalogPath = (localeName) => { 66 | return path.join(getLocalePath(localeName), 'messages.po'); 67 | }; 68 | 69 | /** 70 | * @param {string} localeName 71 | * @returns {string} 72 | */ 73 | const getLocaleCompiledCatalogPath = (localeName) => { 74 | return path.join(getLocalePath(localeName), 'messages.js'); 75 | }; 76 | 77 | const getLocaleMetadataPath = () => { 78 | return path.join(translationsPath, 'LocalesMetadata.js'); 79 | }; 80 | 81 | /** 82 | * @param {string} langCode 83 | * @returns {string} 84 | */ 85 | const getLocaleName = (langCode) => { 86 | if (langCode === 'pt_BR') { 87 | return 'Brazilian Portuguese'; 88 | } else if (langCode === 'zh_CN') { 89 | return 'Chinese Simplified'; 90 | } else if (langCode === 'zh_TW') { 91 | return 'Chinese Traditional'; 92 | } else if (langCode === 'sr_CS') { 93 | return 'Serbian (Latin)'; 94 | } else if (langCode === 'fil_PH') { 95 | return 'Filipino'; 96 | } else if (langCode === 'pseudo_LOCALE') { 97 | return 'for development only'; 98 | } 99 | 100 | // @ts-ignore 101 | return ISO6391.getName(getShortestCode(langCode)); 102 | }; 103 | 104 | /** 105 | * 106 | * @param {string} langCode 107 | * @returns {string} 108 | */ 109 | const getLocaleNativeName = (langCode) => { 110 | if (langCode === 'pt_BR') { 111 | return 'Português brasileiro'; 112 | } else if (langCode === 'zh_CN') { 113 | return '简化字'; 114 | } else if (langCode === 'zh_TW') { 115 | return '正體字'; 116 | } else if (langCode === 'sr_CS') { 117 | return 'srpski'; 118 | } else if (langCode === 'fil_PH') { 119 | return 'Mga Filipino'; 120 | } else if (langCode === 'pseudo_LOCALE') { 121 | return 'Pseudolocalization'; 122 | } 123 | 124 | // @ts-ignore 125 | return ISO6391.getNativeName(getShortestCode(langCode)); 126 | }; 127 | 128 | module.exports = { 129 | getLocales, 130 | getLocalePath, 131 | getLocaleSourceCatalogFiles, 132 | getLocaleCatalogPath, 133 | getLocaleCompiledCatalogPath, 134 | getLocaleMetadataPath, 135 | getLocaleName, 136 | getLocaleNativeName, 137 | }; 138 | -------------------------------------------------------------------------------- /scripts/lib/WikiHelpLink.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @param {string} str */ 4 | const toKebabCase = (str) => { 5 | return str 6 | .replace(/([a-z])([A-Z])/g, '$1-$2') // get all lowercase letters that are near to uppercase ones 7 | .replace(/[\s_]+/g, '-') // replace all spaces and low dash 8 | .toLowerCase(); // convert to lower case 9 | }; 10 | 11 | /** @type {Record<string, string>} */ 12 | const renamedExtensionNames = { 13 | AdMob: 'Admob', 14 | BuiltinFile: 'Storage', 15 | FileSystem: 'Filesystem', 16 | TileMap: 'Tilemap', 17 | BuiltinMouse: 'MouseTouch', 18 | }; 19 | 20 | /** 21 | * @param {string} extensionName 22 | * @returns {string} 23 | */ 24 | const getExtensionFolderName = (extensionName) => { 25 | return toKebabCase( 26 | renamedExtensionNames[extensionName] || 27 | extensionName.replace(/^Builtin/, '') 28 | ); 29 | }; 30 | 31 | /** 32 | * @param {string} extensionName 33 | * @returns {string} 34 | */ 35 | const getExtensionReferencePagePath = (extensionName) => { 36 | const folderName = getExtensionFolderName(extensionName); 37 | const referencePagePath = `/extensions/${folderName}`; 38 | return referencePagePath; 39 | }; 40 | 41 | module.exports = { 42 | getExtensionReferencePagePath, 43 | }; 44 | -------------------------------------------------------------------------------- /scripts/lib/rules/DotsInSentences.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../types").Extension} Extension */ 2 | /** @typedef {import("../../types").EventsFunction} EventsFunction */ 3 | /** @typedef {import("../../types").EventsBasedBehavior} EventsBasedBehaviors */ 4 | /** @typedef {import("../../types").Parameter} Parameter */ 5 | /** @typedef {import("./rule").ErrorLogger} ErrorLogger */ 6 | 7 | /** 8 | * The list of required fields per object type, for usage with {@link checkForForbiddenDot}. 9 | */ 10 | const NO_DOT = { 11 | /** @type {Partial<keyof Extension>[]} */ 12 | EXTENSION: ['name', 'fullName'], 13 | /** @type {Partial<keyof EventsFunction>[]} */ 14 | EXPRESSION: ['name', 'fullName'], 15 | /** @type {Partial<keyof EventsFunction>[]} */ 16 | INSTRUCTION: ['name', 'fullName', 'sentence'], 17 | /** @type {Partial<keyof EventsFunction>[]} */ 18 | ACTION_WITH_OPERATOR: ['name', 'getterName'], 19 | /** @type {Partial<keyof EventsBasedBehaviors>[]} */ 20 | BEHAVIOR: ['name', 'fullName'], 21 | /** @type {Partial<keyof Parameter>[]} */ 22 | PARAMETER: ['name', 'description'], 23 | }; 24 | 25 | /** 26 | * The list of required fields per object type, for usage with {@link checkForForbiddenDot}. 27 | */ 28 | const DOT_REQUIRED = { 29 | /** @type {Partial<keyof Extension>[]} */ 30 | EXTENSION: [ 31 | //'description', - Not enforced since most 32 | 'shortDescription', 33 | ], 34 | /** @type {Partial<keyof EventsFunction>[]} */ 35 | EXPRESSION: ['description'], 36 | /** @type {Partial<keyof EventsFunction>[]} */ 37 | INSTRUCTION: ['description'], 38 | /** @type {Partial<keyof EventsFunction>[]} */ 39 | ACTION_WITH_OPERATOR: [], 40 | /** @type {Partial<keyof EventsBasedBehaviors>[]} */ 41 | BEHAVIOR: ['description'], 42 | }; 43 | 44 | /** 45 | * @param {?(string | string[])} attribute The attribute to trim. 46 | * @returns {string} a trimmed representation of the attribute value. 47 | */ 48 | const trim = function (attribute) { 49 | return attribute 50 | ? // Descriptions are arrays when they have several lines. 51 | Array.isArray(attribute) 52 | ? attribute.join('\n').trim() 53 | : attribute.trim() 54 | : // Some attributes are optionals 55 | ''; 56 | }; 57 | 58 | /** 59 | * Checks if an object has forbidden dots. 60 | * @template {Record<string, any>} T 61 | * @param {T} object The object whose fields need a check. 62 | * @param {Array<keyof T>} fields The fields to check against dots. 63 | * @param {string} sourceName The name of the object to print in the error. 64 | * @param {ErrorLogger} onError The errors list to log any error to. 65 | */ 66 | function checkForForbiddenDot(object, fields, sourceName, onError) { 67 | for (let key of fields) { 68 | const trimmed = trim(object[key]); 69 | // Triple dots is the exception in case two parameters have complementary descriptions 70 | if (trimmed.endsWith('.') && !trimmed.endsWith('...')) 71 | onError( 72 | `Field '${key}' of ${sourceName} has a dot, but it is forbidden there!`, 73 | () => { 74 | // @ts-ignore 75 | object[key] = trimmed.slice(0, -1); 76 | } 77 | ); 78 | } 79 | } 80 | 81 | /** 82 | * Checks if an object misses dots. 83 | * @template {Record<string, string>} T 84 | * @param {T} object The object whose fields need a check. 85 | * @param {Array<keyof T>} fields The fields to check against missing dots. 86 | * @param {string} sourceName The name of the object to print in the error. 87 | * @param {ErrorLogger} onError The errors list to log any error to. 88 | */ 89 | function checkForMissingDot(object, fields, sourceName, onError) { 90 | for (let key of fields) { 91 | const trimmed = trim(object[key]); 92 | if (trimmed.length !== 0 && !trimmed.endsWith('.')) 93 | onError( 94 | `Field '${key}' of ${sourceName} misses a dot at the end of the sentence!`, 95 | () => { 96 | //@ts-ignore Not sure what it is complaining about 97 | object[key] = trimmed + '.'; 98 | } 99 | ); 100 | } 101 | } 102 | 103 | /** 104 | * Checks for missing and forbidden dots. 105 | * @template {Record<string, any>} T 106 | * @param {T} object The object whose fields need a check. 107 | * @param {keyof typeof DOT_REQUIRED} type The fields to check against missing dots. 108 | * @param {string} sourceName The name of the object to print in the error. 109 | * @param {(message: string) => void} onError The errors list to log any error to. 110 | */ 111 | function checkForDots(object, type, sourceName, onError) { 112 | checkForForbiddenDot(object, NO_DOT[type], sourceName, onError); 113 | checkForMissingDot(object, DOT_REQUIRED[type], sourceName, onError); 114 | } 115 | 116 | /** @type {import("./rule").Rule} */ 117 | async function validate({ extension, publicEventsFunctions, onError }) { 118 | checkForDots(extension, 'EXTENSION', 'the extension description', onError); 119 | for (const func of publicEventsFunctions) { 120 | checkForDots( 121 | func, 122 | func.functionType === 'Action' || 123 | func.functionType === 'Condition' || 124 | func.functionType === 'ExpressionAndCondition' 125 | ? 'INSTRUCTION' 126 | : func.functionType === 'ActionWithOperator' 127 | ? 'ACTION_WITH_OPERATOR' 128 | : 'EXPRESSION', 129 | `the function '${func.name}'`, 130 | onError 131 | ); 132 | 133 | for (const parameter of func.parameters) 134 | checkForForbiddenDot( 135 | parameter, 136 | NO_DOT.PARAMETER, 137 | `the function '${func.name} parameter '${parameter.name}'`, 138 | onError 139 | ); 140 | } 141 | for (const behavior of extension.eventsBasedBehaviors) 142 | checkForDots( 143 | behavior, 144 | 'BEHAVIOR', 145 | `the behavior '${behavior.name}'`, 146 | onError 147 | ); 148 | } 149 | 150 | /** @type {import("./rule").RuleModule} */ 151 | module.exports = { 152 | name: 'Dots in sentences', 153 | validate, 154 | }; 155 | -------------------------------------------------------------------------------- /scripts/lib/rules/FilledOutDescriptions.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../types").Extension} Extension */ 2 | /** @typedef {import("../../types").EventsFunction} EventsFunction */ 3 | /** @typedef {import("../../types").EventsBasedBehavior} EventsBasedBehaviors */ 4 | /** @typedef {import("../../types").EventsBasedObject} EventsBasedObject */ 5 | /** @typedef {import("../../types").Parameter} Parameter */ 6 | 7 | /** 8 | * The list of required fields per object type, for usage with {@link checkForFilledOutString}. 9 | */ 10 | const NECESSARY_FIELDS = { 11 | /** @type {Partial<keyof Extension>[]} */ 12 | EXTENSION: ['name', 'fullName', 'description', 'shortDescription'], 13 | /** @type {Partial<keyof EventsFunction>[]} */ 14 | EXPRESSION: ['name', 'fullName', 'description', 'functionType'], 15 | /** @type {Partial<keyof EventsFunction>[]} */ 16 | INSTRUCTION: ['name', 'fullName', 'description', 'functionType', 'sentence'], 17 | /** @type {Partial<keyof EventsFunction>[]} */ 18 | ACTION_WITH_OPERATOR: ['name', 'getterName'], 19 | /** @type {Partial<keyof EventsBasedBehaviors>[]} */ 20 | BEHAVIOR: ['name', 'fullName', 'description'], 21 | /** @type {Partial<keyof EventsBasedObject>[]} */ 22 | OBJECT: ['name', 'fullName', 'description'], 23 | /** @type {Partial<keyof Parameter>[]} */ 24 | PARAMETER: ['description', 'name', 'type'], 25 | }; 26 | 27 | /** 28 | * @param {?(string | string[])} attribute The attribute to trim. 29 | * @returns {string} a trimmed representation of the attribute value. 30 | */ 31 | const trim = function (attribute) { 32 | return attribute 33 | ? // Descriptions are arrays when they have several lines. 34 | Array.isArray(attribute) 35 | ? attribute.join('\n').trim() 36 | : attribute.trim() 37 | : // Some attributes are optionals 38 | ''; 39 | }; 40 | 41 | /** 42 | * Checks if an object string fields are filled out. 43 | * @template {Record<string, any>} T 44 | * @param {T} object The object to check for fields. 45 | * @param {Array<keyof T>} fields The fields to check against emptiness. 46 | * @param {string} sourceName The name of the object to print in the error. 47 | * @param {(message: string) => void} onError The errors list to log any error to. 48 | */ 49 | function checkForFilledOutString(object, fields, sourceName, onError) { 50 | for (let key of fields) { 51 | if (trim(object[key]).length === 0) 52 | onError(`Required field '${key}' of ${sourceName} is not filled out!`); 53 | } 54 | } 55 | 56 | /** @type {import("./rule").Rule} */ 57 | async function validate({ extension, publicEventsFunctions, onError }) { 58 | // Check if necessary fields are filled out 59 | checkForFilledOutString( 60 | extension, 61 | NECESSARY_FIELDS.EXTENSION, 62 | 'the extension description', 63 | onError 64 | ); 65 | for (const func of publicEventsFunctions) { 66 | checkForFilledOutString( 67 | func, 68 | func.functionType === 'Action' || 69 | func.functionType === 'Condition' || 70 | func.functionType === 'ExpressionAndCondition' 71 | ? NECESSARY_FIELDS.INSTRUCTION 72 | : func.functionType === 'ActionWithOperator' 73 | ? NECESSARY_FIELDS.ACTION_WITH_OPERATOR 74 | : NECESSARY_FIELDS.EXPRESSION, 75 | `the function '${func.name}'`, 76 | onError 77 | ); 78 | 79 | for (const parameter of func.parameters) 80 | checkForFilledOutString( 81 | parameter, 82 | NECESSARY_FIELDS.PARAMETER, 83 | `the function '${func.name} parameter '${parameter.name}'`, 84 | onError 85 | ); 86 | } 87 | for (const behavior of extension.eventsBasedBehaviors) 88 | checkForFilledOutString( 89 | behavior, 90 | NECESSARY_FIELDS.BEHAVIOR, 91 | `the behavior '${behavior.name}'`, 92 | onError 93 | ); 94 | if (extension.eventsBasedObjects) { 95 | for (const object of extension.eventsBasedObjects) { 96 | checkForFilledOutString( 97 | object, 98 | NECESSARY_FIELDS.OBJECT, 99 | `the object '${object.name}'`, 100 | onError 101 | ); 102 | } 103 | } 104 | } 105 | 106 | /** @type {import("./rule").RuleModule} */ 107 | module.exports = { 108 | name: 'Filled out names and descriptions', 109 | validate, 110 | }; 111 | -------------------------------------------------------------------------------- /scripts/lib/rules/HasCorrectInternalName.js: -------------------------------------------------------------------------------- 1 | const { isValidExtensionName } = require('../ExtensionNameValidator'); 2 | 3 | /** @type {import("./rule").Rule} */ 4 | async function validate({ extension, onError }) { 5 | if (!isValidExtensionName(extension.name)) 6 | onError( 7 | `The internal name of the extension ${extension.name} is invalid! It should only contain normal latin upper- and lowercase characters and numbers. The first letter must be an uppercase character.` 8 | ); 9 | } 10 | 11 | /** @type {import("./rule").RuleModule} */ 12 | module.exports = { 13 | name: 'Internal name validity', 14 | validate, 15 | }; 16 | -------------------------------------------------------------------------------- /scripts/lib/rules/JavascriptDisallowedProperties.js: -------------------------------------------------------------------------------- 1 | const { 2 | extensionsAllowedProperties, 3 | } = require('../ExtensionsValidatorExceptions.js'); 4 | const { inspect } = require('util'); 5 | 6 | /** @typedef {import('../../types.js').ExtensionAllowedProperties} ExtensionAllowedProperties */ 7 | /** @typedef {import('../../types.js').DisallowedPropertyError} DisallowedPropertyError */ 8 | 9 | /** @type {ExtensionAllowedProperties} */ 10 | const emptyExtensionAllowedProperties = { 11 | gdjsEvtToolsAllowedProperties: [], 12 | gdjsAllowedProperties: [], 13 | runtimeSceneAllowedProperties: [], 14 | javaScriptObjectAllowedProperties: [], 15 | }; 16 | 17 | /** 18 | * @param {Set<string>} allowedProperties 19 | * @param {Set<string>} properties 20 | */ 21 | const findDisallowedProperties = (allowedProperties, properties) => { 22 | return [...properties].filter((property) => !allowedProperties.has(property)); 23 | }; 24 | 25 | /** 26 | * Check the extension for any properties that are not on the allow list. 27 | * @param {Object} parsedExtensionJson 28 | * @returns {DisallowedPropertyError[]} 29 | */ 30 | const checkExtensionForDisallowedProperties = (parsedExtensionJson) => { 31 | // @ts-ignore 32 | const name = parsedExtensionJson.name; 33 | const extensionJsonString = JSON.stringify(parsedExtensionJson); 34 | 35 | /** @type {ExtensionAllowedProperties} */ 36 | const extensionSpecificAllowance = 37 | extensionsAllowedProperties.extensionSpecificAllowance[name] || 38 | emptyExtensionAllowedProperties; 39 | const anyExtensionAllowance = 40 | extensionsAllowedProperties.anyExtensionAllowance; 41 | 42 | const gdjsAllowedProperties = new Set([ 43 | ...anyExtensionAllowance.gdjsAllowedProperties, 44 | ...extensionSpecificAllowance.gdjsAllowedProperties, 45 | ]); 46 | const gdjsEvtToolsAllowedProperties = new Set([ 47 | ...anyExtensionAllowance.gdjsEvtToolsAllowedProperties, 48 | ...extensionSpecificAllowance.gdjsEvtToolsAllowedProperties, 49 | ]); 50 | const runtimeSceneAllowedProperties = new Set([ 51 | ...anyExtensionAllowance.runtimeSceneAllowedProperties, 52 | ...extensionSpecificAllowance.runtimeSceneAllowedProperties, 53 | ]); 54 | const javaScriptObjectAllowedProperties = new Set([ 55 | ...anyExtensionAllowance.javaScriptObjectAllowedProperties, 56 | ...extensionSpecificAllowance.javaScriptObjectAllowedProperties, 57 | ]); 58 | 59 | /** @type {DisallowedPropertyError[]} */ 60 | const disallowedPropertyErrors = []; 61 | 62 | { 63 | const gdjsPropertiesRegex = /gdjs\.\s*(\w+)/g; 64 | const matches = [...extensionJsonString.matchAll(gdjsPropertiesRegex)]; 65 | const properties = new Set(matches.map((match) => match[1])); 66 | findDisallowedProperties(gdjsAllowedProperties, properties).forEach( 67 | (disallowedProperty) => { 68 | disallowedPropertyErrors.push({ 69 | allowedProperties: Array.from(gdjsAllowedProperties), 70 | disallowedProperty: disallowedProperty, 71 | objectName: 'gdjs', 72 | }); 73 | } 74 | ); 75 | } 76 | { 77 | const gdjsEvtToolsPropertiesRegex = /gdjs\.\s*evtTools\.\s*(\w+)/g; 78 | const matches = [ 79 | ...extensionJsonString.matchAll(gdjsEvtToolsPropertiesRegex), 80 | ]; 81 | const properties = new Set(matches.map((match) => match[1])); 82 | findDisallowedProperties(gdjsEvtToolsAllowedProperties, properties).forEach( 83 | (disallowedProperty) => { 84 | disallowedPropertyErrors.push({ 85 | allowedProperties: Array.from(gdjsEvtToolsAllowedProperties), 86 | disallowedProperty: disallowedProperty, 87 | objectName: 'gdjs.evtTools', 88 | }); 89 | } 90 | ); 91 | } 92 | { 93 | const runtimeScenePropertiesRegex = /runtimeScene\.\s*(\w+)/g; 94 | const matches = [ 95 | ...extensionJsonString.matchAll(runtimeScenePropertiesRegex), 96 | ]; 97 | const properties = new Set(matches.map((match) => match[1])); 98 | findDisallowedProperties(runtimeSceneAllowedProperties, properties).forEach( 99 | (disallowedProperty) => { 100 | disallowedPropertyErrors.push({ 101 | allowedProperties: Array.from(runtimeSceneAllowedProperties), 102 | disallowedProperty: disallowedProperty, 103 | objectName: 'runtimeScene', 104 | }); 105 | } 106 | ); 107 | } 108 | { 109 | const javaScriptObjectPropertiesRegex = /\s+Object\.\s*([a-z]\w+)/g; 110 | const matches = [ 111 | ...extensionJsonString.matchAll(javaScriptObjectPropertiesRegex), 112 | ]; 113 | const properties = new Set(matches.map((match) => match[1])); 114 | findDisallowedProperties( 115 | javaScriptObjectAllowedProperties, 116 | properties 117 | ).forEach((disallowedProperty) => { 118 | disallowedPropertyErrors.push({ 119 | allowedProperties: Array.from(javaScriptObjectAllowedProperties), 120 | disallowedProperty: disallowedProperty, 121 | objectName: 'Object', 122 | }); 123 | }); 124 | } 125 | 126 | return disallowedPropertyErrors; 127 | }; 128 | 129 | /** @type {import("./rule").Rule} */ 130 | async function validate({ extension, onError }) { 131 | // Check for disallowed properties 132 | const disallowedPropertyErrors = 133 | checkExtensionForDisallowedProperties(extension); 134 | if (disallowedPropertyErrors.length > 0) { 135 | const reducedError = disallowedPropertyErrors 136 | .reduce( 137 | (accumulator, current) => 138 | accumulator + 139 | inspect(current, undefined, undefined, process.env.CI !== 'true') + 140 | '\n', 141 | `Found disallowed properties in extension '${extension.name}':\n` 142 | ) 143 | // Remove the last \n 144 | .slice(0, -1); 145 | onError(reducedError); 146 | } 147 | } 148 | 149 | /** @type {import("./rule").RuleModule} */ 150 | module.exports = { 151 | name: 'JavaScript disallowed properties', 152 | validate, 153 | ignoreDuringPreliminaryChecks: true, 154 | }; 155 | -------------------------------------------------------------------------------- /scripts/lib/rules/NameConsistency.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./rule").Rule} */ 2 | async function validate({ extension: { name }, filename, onError }) { 3 | // Do some base consistency checks. 4 | if (name.endsWith('Extension')) { 5 | onError( 6 | `Extension names should not finish with \"Extension\". Please rename '${name}' to '${name.slice( 7 | 0, 8 | -9 9 | )}'.` 10 | ); 11 | } 12 | 13 | const expectedFilename = name + '.json'; 14 | if (expectedFilename !== filename) { 15 | onError( 16 | `Extension filename should be exactly the name of the extension (with .json extension). Please rename '${filename}' to '${expectedFilename}'.` 17 | ); 18 | } 19 | } 20 | 21 | /** @type {import("./rule").RuleModule} */ 22 | module.exports = { 23 | name: 'Extension name consistency', 24 | validate, 25 | }; 26 | -------------------------------------------------------------------------------- /scripts/lib/rules/NoGet.js: -------------------------------------------------------------------------------- 1 | const { 2 | legacyGetPrefixedExpressionsExtensions, 3 | } = require('../ExtensionsValidatorExceptions'); 4 | 5 | /** @type {import("./rule").Rule} */ 6 | async function validate({ extension, publicEventsFunctions, onError }) { 7 | // Check that expressions are not prefixed with 'Get' 8 | for (const func of publicEventsFunctions) { 9 | const { name, functionType } = func; 10 | if ( 11 | (functionType === 'Expression' || functionType === 'StringExpression') && 12 | name.startsWith('Get') && 13 | !legacyGetPrefixedExpressionsExtensions.has(extension.name) 14 | ) 15 | onError(`Expression '${name}' is using prohibited 'Get' prefix!`); 16 | } 17 | } 18 | 19 | /** @type {import("./rule").RuleModule} */ 20 | module.exports = { 21 | name: "No 'Get' in expressions", 22 | validate, 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/lib/rules/PascalCase.js: -------------------------------------------------------------------------------- 1 | const { 2 | legacyCamelCaseExtensions, 3 | } = require('../ExtensionsValidatorExceptions'); 4 | 5 | /** 6 | * Check if an internal name is in "PascalCase". 7 | * Note that no real PascalCase is enforced, because it does not 8 | * make sense to use PascalCase for certain names. 9 | * (MQTT > Mqtt, URLTools > Urltools) 10 | * 11 | * Therefore we just check if at least the first character is uppercase 12 | * to be sure that at least it is not camelCase that is used. 13 | * 14 | * @param {string} name The name to check. 15 | * @param {(message: string)=>void} onError Callback function to log errors with. 16 | */ 17 | function checkPascalCase(name, onError) { 18 | if (name[0] !== name[0].toUpperCase()) 19 | onError( 20 | `Internal name '${name}' should begin with an uppercase letter (${ 21 | name[0].toUpperCase() + name.slice(1) 22 | })!` 23 | ); 24 | } 25 | 26 | /** @type {import("./rule").Rule} */ 27 | async function validate({ 28 | extension: { name, eventsBasedBehaviors }, 29 | publicEventsFunctions, 30 | onError, 31 | }) { 32 | if (legacyCamelCaseExtensions.has(name)) return; 33 | for (const { name } of publicEventsFunctions) checkPascalCase(name, onError); 34 | for (const { name } of eventsBasedBehaviors) checkPascalCase(name, onError); 35 | } 36 | 37 | /** @type {import("./rule").RuleModule} */ 38 | module.exports = { 39 | name: 'PascalCase for internals names', 40 | validate, 41 | }; 42 | -------------------------------------------------------------------------------- /scripts/lib/rules/SemVer.js: -------------------------------------------------------------------------------- 1 | const { valid: isValidSemver } = require('semver'); 2 | 3 | /** @type {import("./rule").Rule} */ 4 | async function validate({ extension: { version }, onError }) { 5 | if (!isValidSemver(version)) 6 | onError( 7 | `Version '${version}' is not a semantic versioning compliant version number!` 8 | ); 9 | } 10 | 11 | /** @type {import("./rule").RuleModule} */ 12 | module.exports = { 13 | name: 'Semantic versioning', 14 | validate, 15 | }; 16 | -------------------------------------------------------------------------------- /scripts/lib/rules/_Template.js: -------------------------------------------------------------------------------- 1 | /** @type {import("./rule").Rule} */ 2 | async function validate({ extension, onError }) {} 3 | 4 | /** @type {import("./rule").RuleModule} */ 5 | module.exports = { 6 | name: 'Rule name', 7 | validate, 8 | }; 9 | -------------------------------------------------------------------------------- /scripts/lib/rules/rule.d.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionWithProperFileInfo, EventsFunction } from '../../types'; 2 | 3 | export type Rule = (context: RuleContext) => Promise<void>; 4 | export type ErrorLogger = (error: string, fix?: () => void) => void; 5 | 6 | export interface RuleContext extends ExtensionWithProperFileInfo { 7 | onError: ErrorLogger; 8 | publicEventsFunctions: EventsFunction[]; 9 | allEventsFunctions: EventsFunction[]; 10 | } 11 | 12 | export interface RuleModule { 13 | name: string; 14 | validate: Rule; 15 | ignoreDuringPreliminaryChecks?: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /scripts/types.d.ts: -------------------------------------------------------------------------------- 1 | interface ItemExtensionHeaderFields { 2 | authorIds: Array<string>; 3 | extensionNamespace: string; 4 | version: string; 5 | gdevelopVersion?: string; 6 | tags: Array<string>; 7 | category: string; 8 | previewIconUrl: string; 9 | changelog?: Array<{ version: string; breaking?: string }>; 10 | } 11 | 12 | type ExtensionTier = 'community' | 'reviewed'; 13 | 14 | /** 15 | * An extension, behavior or object. 16 | */ 17 | export interface RegistryItem extends ItemExtensionHeaderFields { 18 | tier: ExtensionTier; 19 | url: string; 20 | headerUrl: string; 21 | } 22 | 23 | interface ExtensionAndShortHeaderFields extends ItemExtensionHeaderFields { 24 | shortDescription: string; 25 | fullName: string; 26 | name: string; 27 | helpPath: string; 28 | } 29 | 30 | interface ExtensionAndHeaderFields { 31 | description: string; 32 | iconUrl: string; 33 | } 34 | 35 | export interface EventsFunctionInsideExtensionShortHeader { 36 | description: string; 37 | fullName: string; 38 | functionType: 39 | | 'StringExpression' 40 | | 'Expression' 41 | | 'Action' 42 | | 'Condition' 43 | | 'ExpressionAndCondition' 44 | | 'ActionWithOperator'; 45 | name: string; 46 | } 47 | 48 | export interface EventsBasedBehaviorInsideExtensionShortHeader { 49 | description: string; 50 | fullName: string; 51 | name: string; 52 | objectType: string; 53 | eventsFunctions: EventsFunctionInsideExtensionShortHeader[]; 54 | } 55 | 56 | export interface EventsBasedObjectInsideExtensionShortHeader { 57 | description: string; 58 | fullName: string; 59 | name: string; 60 | defaultName: string; 61 | eventsFunctions: EventsFunctionInsideExtensionShortHeader[]; 62 | } 63 | 64 | export interface ExtensionShortHeader 65 | extends RegistryItem, 66 | ExtensionAndShortHeaderFields { 67 | tier: ExtensionTier; 68 | url: string; 69 | headerUrl: string; 70 | /** Only defined for "reviewed" extensions. */ 71 | eventsBasedBehaviors?: EventsBasedBehaviorInsideExtensionShortHeader[]; 72 | eventsBasedBehaviorsCount: number; 73 | /** Only defined for "reviewed" extensions. */ 74 | eventsFunctions?: EventsFunctionInsideExtensionShortHeader[]; 75 | eventsFunctionsCount: number; 76 | /** Only defined for "reviewed" extensions. */ 77 | eventsBasedObjects?: EventsBasedObjectInsideExtensionShortHeader[]; 78 | } 79 | 80 | interface BehaviorAndShortHeaderFields { 81 | description: string; 82 | fullName: string; 83 | name: string; 84 | objectType: string; 85 | } 86 | 87 | export interface BehaviorShortHeader 88 | extends RegistryItem, 89 | BehaviorAndShortHeaderFields { 90 | extensionName: string; 91 | /** 92 | * All required behaviors including transitive ones. 93 | */ 94 | allRequiredBehaviorTypes: Array<string>; 95 | } 96 | 97 | interface ObjectAndShortHeaderFields { 98 | description: string; 99 | fullName: string; 100 | name: string; 101 | } 102 | 103 | export interface ObjectShortHeader 104 | extends RegistryItem, 105 | ObjectAndShortHeaderFields { 106 | extensionName: string; 107 | } 108 | 109 | export interface ExtensionHeader 110 | extends ExtensionShortHeader, 111 | ExtensionAndHeaderFields {} 112 | 113 | export interface ExtensionsDatabase { 114 | version: string; 115 | /** @deprecated Tags list should be built by the UI. When only reviewed 116 | * extensions are shown, some tags could lead to no extension. */ 117 | allTags: Array<string>; 118 | /** @deprecated Categories list should be built by the UI. */ 119 | allCategories: Array<string>; 120 | extensionShortHeaders: Array<ExtensionShortHeader>; 121 | behavior: { 122 | headers: Array<BehaviorShortHeader>; 123 | views: { 124 | default: { 125 | firstIds: Array<{ extensionName: string; behaviorName: string }>; 126 | }; 127 | }; 128 | }; 129 | object: { 130 | headers: Array<ObjectShortHeader>; 131 | views: { 132 | default: { 133 | firstIds: Array<{ extensionName: string; objectName: string }>; 134 | }; 135 | }; 136 | }; 137 | views: { 138 | default: { 139 | firstExtensionIds: Array<string>; 140 | }; 141 | }; 142 | } 143 | 144 | /** 145 | * A list of properties, either from the game engine public API (https://docs.gdevelop-app.com/GDJS%20Runtime%20Documentation/index.html), 146 | * or custom properties, that can be used by an extension. 147 | */ 148 | export interface ExtensionAllowedProperties { 149 | gdjsAllowedProperties: Array<string>; 150 | gdjsEvtToolsAllowedProperties: Array<string>; 151 | runtimeSceneAllowedProperties: Array<string>; 152 | javaScriptObjectAllowedProperties: Array<string>; 153 | } 154 | 155 | export interface DisallowedPropertyError { 156 | objectName: string; 157 | disallowedProperty: string; 158 | allowedProperties: string[]; 159 | } 160 | 161 | export interface Parameter { 162 | codeOnly: boolean; 163 | defaultValue: string; 164 | description: string; 165 | longDescription: string; 166 | name: string; 167 | optional: boolean; 168 | supplementaryInformation: string; 169 | type: 'expression'; 170 | } 171 | 172 | export interface EventsFunction { 173 | description: string; 174 | fullName: string; 175 | functionType: 176 | | 'StringExpression' 177 | | 'Expression' 178 | | 'Action' 179 | | 'Condition' 180 | | 'ExpressionAndCondition' 181 | | 'ActionWithOperator'; 182 | name: string; 183 | getterName: string; 184 | private: boolean; 185 | sentence: string; 186 | events: any[]; 187 | parameters: Parameter[]; 188 | objectGroups: string[]; 189 | } 190 | 191 | export interface PropertyDescriptor { 192 | type: 'Number' | 'String' | 'Boolean' | 'Choice' | 'Color' | 'Behavior'; 193 | extraInformation: string[]; 194 | } 195 | 196 | export interface EventsBasedBehavior { 197 | description: string; 198 | fullName: string; 199 | name: string; 200 | objectType: string; 201 | private?: boolean; 202 | eventsFunctions: EventsFunction[]; 203 | propertyDescriptors: PropertyDescriptor[]; 204 | } 205 | 206 | export interface EventsBasedObject { 207 | description: string; 208 | fullName: string; 209 | name: string; 210 | defaultName: string; 211 | private?: boolean; 212 | eventsFunctions: EventsFunction[]; 213 | } 214 | 215 | export interface Extension 216 | extends ExtensionAndShortHeaderFields, 217 | ExtensionAndHeaderFields { 218 | tags: string | string[]; 219 | eventsFunctions: EventsFunction[]; 220 | eventsBasedBehaviors: EventsBasedBehavior[]; 221 | eventsBasedObjects?: EventsBasedObject[]; 222 | } 223 | 224 | export interface ExtensionWithProperFileInfo { 225 | state: 'success'; 226 | filename: string; 227 | tier: ExtensionTier; 228 | extension: Extension; 229 | } 230 | interface ExtensionWithErroredFileInfo { 231 | state: 'error'; 232 | filename: string; 233 | tier: ExtensionTier; 234 | error: Error; 235 | } 236 | 237 | export type ExtensionWithFileInfo = 238 | | ExtensionWithProperFileInfo 239 | | ExtensionWithErroredFileInfo; 240 | 241 | export interface Error { 242 | message: `[${string}]: ${string}`; 243 | fix?: () => void; 244 | } 245 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "allowJs": true, 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "strictBindCallApply": true, 12 | "strictPropertyInitialization": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | 16 | /* Additional Checks */ 17 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 20 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 21 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 22 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 23 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 24 | 25 | "moduleResolution": "node", 26 | "esModuleInterop": true, 27 | "resolveJsonModule": true, 28 | "lib": ["ES2020"], 29 | "skipLibCheck": true, 30 | "forceConsistentCasingInFileNames": true 31 | }, 32 | "include": ["scripts", "__tests__", "lib.es5.d.ts"] 33 | } 34 | --------------------------------------------------------------------------------