From aa145bfa64023ea14ef5f4cbdfb627c8308bccc3 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Mon, 11 May 2026 18:45:04 +0200 Subject: [PATCH 01/17] Extract local editor settings sidecar helper --- .../LocalEditorSettingsStorage.js | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js new file mode 100644 index 000000000000..27dbfa9d8d36 --- /dev/null +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -0,0 +1,219 @@ +// @flow +import optionalRequire from '../../Utils/OptionalRequire'; + +const path = optionalRequire('path'); + +export const editorSettingsDirectoryName = '.gdevelop'; +export const editorSettingsFileName = 'editor-settings.json'; + +export type ProjectEditorSettings = {| + layoutSettings: { [layoutName: string]: Object }, + externalLayoutSettings: { [externalLayoutName: string]: Object }, + eventsBasedObjectVariantSettings: { + [variantIdentifier: string]: Object, + }, +|}; + +export const getProjectEditorSettingsFilePath = ( + projectPath: string +): string => { + if (!path) { + throw new Error('Filesystem paths are not supported.'); + } + + return path.join( + projectPath, + editorSettingsDirectoryName, + editorSettingsFileName + ); +}; + +const makeProjectEditorSettings = (): ProjectEditorSettings => ({ + layoutSettings: {}, + externalLayoutSettings: {}, + eventsBasedObjectVariantSettings: {}, +}); + +const hasOwn = (object: Object, propertyName: string): boolean => + Object.prototype.hasOwnProperty.call(object, propertyName); + +const isEmpty = (object: Object): boolean => Object.keys(object).length === 0; + +const makeVariantIdentifier = ( + extensionName: string, + objectName: string, + variantName: string +): string => `${extensionName}::${objectName}::${variantName}`; + +export const extractProjectEditorSettings = ( + serializedProjectObject: Object +): ?ProjectEditorSettings => { + const projectEditorSettings = makeProjectEditorSettings(); + + if (Array.isArray(serializedProjectObject.layouts)) { + serializedProjectObject.layouts.forEach(layout => { + if ( + layout && + typeof layout.name === 'string' && + hasOwn(layout, 'uiSettings') + ) { + projectEditorSettings.layoutSettings[layout.name] = layout.uiSettings; + delete layout.uiSettings; + } + }); + } + + if (Array.isArray(serializedProjectObject.externalLayouts)) { + serializedProjectObject.externalLayouts.forEach(externalLayout => { + if ( + externalLayout && + typeof externalLayout.name === 'string' && + hasOwn(externalLayout, 'editionSettings') + ) { + projectEditorSettings.externalLayoutSettings[ + externalLayout.name + ] = externalLayout.editionSettings; + delete externalLayout.editionSettings; + } + }); + } + + if (Array.isArray(serializedProjectObject.eventsFunctionsExtensions)) { + serializedProjectObject.eventsFunctionsExtensions.forEach(extension => { + if ( + !extension || + typeof extension.name !== 'string' || + !Array.isArray(extension.eventsBasedObjects) + ) { + return; + } + + extension.eventsBasedObjects.forEach(eventsBasedObject => { + if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { + return; + } + + if (hasOwn(eventsBasedObject, 'editionSettings')) { + projectEditorSettings.eventsBasedObjectVariantSettings[ + makeVariantIdentifier(extension.name, eventsBasedObject.name, '') + ] = eventsBasedObject.editionSettings; + delete eventsBasedObject.editionSettings; + } + + if (Array.isArray(eventsBasedObject.variants)) { + eventsBasedObject.variants.forEach(variant => { + if ( + variant && + typeof variant.name === 'string' && + hasOwn(variant, 'editionSettings') + ) { + projectEditorSettings.eventsBasedObjectVariantSettings[ + makeVariantIdentifier( + extension.name, + eventsBasedObject.name, + variant.name + ) + ] = variant.editionSettings; + delete variant.editionSettings; + } + }); + } + }); + }); + } + + if ( + isEmpty(projectEditorSettings.layoutSettings) && + isEmpty(projectEditorSettings.externalLayoutSettings) && + isEmpty(projectEditorSettings.eventsBasedObjectVariantSettings) + ) { + return null; + } + + return projectEditorSettings; +}; + +export const applyProjectEditorSettings = ( + serializedProjectObject: Object, + projectEditorSettings: ?ProjectEditorSettings +) => { + if (!projectEditorSettings) return; + + const { layoutSettings } = projectEditorSettings; + if (layoutSettings && Array.isArray(serializedProjectObject.layouts)) { + serializedProjectObject.layouts.forEach(layout => { + if ( + layout && + typeof layout.name === 'string' && + layoutSettings[layout.name] + ) { + layout.uiSettings = layoutSettings[layout.name]; + } + }); + } + + const { externalLayoutSettings } = projectEditorSettings; + if ( + externalLayoutSettings && + Array.isArray(serializedProjectObject.externalLayouts) + ) { + serializedProjectObject.externalLayouts.forEach(externalLayout => { + if ( + externalLayout && + typeof externalLayout.name === 'string' && + externalLayoutSettings[externalLayout.name] + ) { + externalLayout.editionSettings = + externalLayoutSettings[externalLayout.name]; + } + }); + } + + const { eventsBasedObjectVariantSettings } = projectEditorSettings; + if ( + eventsBasedObjectVariantSettings && + Array.isArray(serializedProjectObject.eventsFunctionsExtensions) + ) { + serializedProjectObject.eventsFunctionsExtensions.forEach(extension => { + if ( + !extension || + typeof extension.name !== 'string' || + !Array.isArray(extension.eventsBasedObjects) + ) { + return; + } + + extension.eventsBasedObjects.forEach(eventsBasedObject => { + if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { + return; + } + + const defaultVariantSettings = + eventsBasedObjectVariantSettings[ + makeVariantIdentifier(extension.name, eventsBasedObject.name, '') + ]; + if (defaultVariantSettings) { + eventsBasedObject.editionSettings = defaultVariantSettings; + } + + if (Array.isArray(eventsBasedObject.variants)) { + eventsBasedObject.variants.forEach(variant => { + if (!variant || typeof variant.name !== 'string') return; + + const variantSettings = + eventsBasedObjectVariantSettings[ + makeVariantIdentifier( + extension.name, + eventsBasedObject.name, + variant.name + ) + ]; + if (variantSettings) { + variant.editionSettings = variantSettings; + } + }); + } + }); + }); + } +}; From 8e44d530cc844ac4807888c3127912b553ae4f33 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Mon, 11 May 2026 18:48:11 +0200 Subject: [PATCH 02/17] Add local editor settings storage tests --- .../LocalEditorSettingsStorage.spec.js | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js new file mode 100644 index 000000000000..e6b72b8fa3ed --- /dev/null +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js @@ -0,0 +1,123 @@ +// @flow +import { + applyProjectEditorSettings, + extractProjectEditorSettings, +} from './LocalEditorSettingsStorage'; + +describe('LocalEditorSettingsStorage', () => { + it('extracts editor settings from project content and applies them back', () => { + const serializedProjectObject: any = { + layouts: [ + { + name: 'Level 1', + uiSettings: { + grid: true, + zoomFactor: 0.5, + }, + }, + { + name: 'Level 2', + }, + ], + externalLayouts: [ + { + name: 'HUD', + editionSettings: { + windowMask: true, + }, + }, + ], + eventsFunctionsExtensions: [ + { + name: 'Inventory', + eventsBasedObjects: [ + { + name: 'Slot', + editionSettings: { + selectedLayer: 'Base', + }, + variants: [ + { + name: 'Large', + editionSettings: { + gridWidth: 64, + }, + }, + ], + }, + ], + }, + ], + }; + + const editorSettings = extractProjectEditorSettings( + serializedProjectObject + ); + + expect(editorSettings).toEqual({ + layoutSettings: { + 'Level 1': { + grid: true, + zoomFactor: 0.5, + }, + }, + externalLayoutSettings: { + HUD: { + windowMask: true, + }, + }, + eventsBasedObjectVariantSettings: { + 'Inventory::Slot::': { + selectedLayer: 'Base', + }, + 'Inventory::Slot::Large': { + gridWidth: 64, + }, + }, + }); + expect(serializedProjectObject.layouts[0].uiSettings).toBeUndefined(); + expect( + serializedProjectObject.externalLayouts[0].editionSettings + ).toBeUndefined(); + expect( + serializedProjectObject.eventsFunctionsExtensions[0].eventsBasedObjects[0] + .editionSettings + ).toBeUndefined(); + expect( + serializedProjectObject.eventsFunctionsExtensions[0].eventsBasedObjects[0] + .variants[0].editionSettings + ).toBeUndefined(); + + applyProjectEditorSettings(serializedProjectObject, editorSettings); + + expect(serializedProjectObject.layouts[0].uiSettings).toEqual({ + grid: true, + zoomFactor: 0.5, + }); + expect(serializedProjectObject.externalLayouts[0].editionSettings).toEqual({ + windowMask: true, + }); + expect( + serializedProjectObject.eventsFunctionsExtensions[0].eventsBasedObjects[0] + .editionSettings + ).toEqual({ + selectedLayer: 'Base', + }); + expect( + serializedProjectObject.eventsFunctionsExtensions[0].eventsBasedObjects[0] + .variants[0].editionSettings + ).toEqual({ + gridWidth: 64, + }); + }); + + it('returns null when there are no editor settings to extract', () => { + expect( + extractProjectEditorSettings({ + layouts: [{ name: 'Level 1' }], + externalLayouts: [], + eventsFunctionsExtensions: [], + }) + ).toBe(null); + }); +}); From 58153c356be6e304a8bb419484d212ebf5966ab9 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Mon, 11 May 2026 18:49:37 +0200 Subject: [PATCH 03/17] Persist local editor settings separately --- .../LocalProjectWriter.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js index 1bc87e340f5e..9824dd706347 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js @@ -19,6 +19,10 @@ import { splitPaths, getSlugifiedUniqueNameFromProperty, } from '../../Utils/ObjectSplitter'; +import { + extractProjectEditorSettings, + getProjectEditorSettingsFilePath, +} from './LocalEditorSettingsStorage'; import type { MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow'; import LocalFolderPicker from '../../UI/LocalFolderPicker'; import SaveAsOptionsDialog from '../SaveAsOptionsDialog'; @@ -131,8 +135,20 @@ const writeProjectFiles = async ({ } else { serializedProjectObject = serializeToJSObject(project); } + const projectEditorSettings = extractProjectEditorSettings( + serializedProjectObject + ); const serializeEndTime = Date.now(); + if (projectEditorSettings) { + await writeAndCheckFormattedJSONFile( + projectEditorSettings, + getProjectEditorSettingsFilePath(projectPath) + ); + } else { + await fs.remove(getProjectEditorSettingsFilePath(projectPath)); + } + if (project.isFolderProject()) { const partialObjects = split(serializedProjectObject, { pathSeparator: '/', From 587bf104d8932344dfc8ee3793f96da191144542 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Mon, 11 May 2026 18:50:05 +0200 Subject: [PATCH 04/17] Load local editor settings sidecar --- .../LocalProjectOpener.js | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js index 19805526b1cb..df2fb8165435 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js @@ -3,6 +3,10 @@ import optionalRequire from '../../Utils/OptionalRequire'; import { type FileMetadata } from '../index'; import { unsplit } from '../../Utils/ObjectSplitter'; import { openFilePicker, readJSONFile } from '../../Utils/FileSystem'; +import { + applyProjectEditorSettings, + getProjectEditorSettingsFilePath, +} from './LocalEditorSettingsStorage'; const fs = optionalRequire('fs'); const path = optionalRequire('path'); @@ -34,9 +38,25 @@ export const onOpen = ( // to be un-splitted, but not the content of these properties), to avoid very slow processing // of large game files. maxUnsplitDepth: 3, - }).then(() => { - return { content: object }; - }); + }) + .then(() => + readJSONFile(getProjectEditorSettingsFilePath(projectPath)) + .then(projectEditorSettings => { + applyProjectEditorSettings(object, projectEditorSettings); + }) + .catch(error => { + if (error && error.code === 'ENOENT') { + return; + } + console.warn( + 'Unable to read project editor settings. Opening the project without them.', + error + ); + }) + ) + .then(() => { + return { content: object }; + }); }); }; From 4e7ee039b25cf37d73d7c6ce10a00a34c83f68b8 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Tue, 12 May 2026 16:54:19 +0200 Subject: [PATCH 05/17] Fix local editor settings Flow check --- .../LocalFileStorageProvider/LocalEditorSettingsStorage.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js index 27dbfa9d8d36..d26b4dc298f4 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -35,7 +35,7 @@ const makeProjectEditorSettings = (): ProjectEditorSettings => ({ }); const hasOwn = (object: Object, propertyName: string): boolean => - Object.prototype.hasOwnProperty.call(object, propertyName); + Object.keys(object).indexOf(propertyName) !== -1; const isEmpty = (object: Object): boolean => Object.keys(object).length === 0; @@ -70,9 +70,8 @@ export const extractProjectEditorSettings = ( typeof externalLayout.name === 'string' && hasOwn(externalLayout, 'editionSettings') ) { - projectEditorSettings.externalLayoutSettings[ - externalLayout.name - ] = externalLayout.editionSettings; + projectEditorSettings.externalLayoutSettings[externalLayout.name] = + externalLayout.editionSettings; delete externalLayout.editionSettings; } }); From aef3de5cd123812246d49a10dc940326053bf68c Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Tue, 12 May 2026 16:55:07 +0200 Subject: [PATCH 06/17] Avoid warning when local editor settings are absent --- .../LocalProjectOpener.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js index df2fb8165435..f5df97ce9371 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js @@ -39,21 +39,25 @@ export const onOpen = ( // of large game files. maxUnsplitDepth: 3, }) - .then(() => - readJSONFile(getProjectEditorSettingsFilePath(projectPath)) + .then(() => { + const projectEditorSettingsFilePath = getProjectEditorSettingsFilePath( + projectPath + ); + if (!fs.existsSync(projectEditorSettingsFilePath)) { + return; + } + + return readJSONFile(projectEditorSettingsFilePath) .then(projectEditorSettings => { applyProjectEditorSettings(object, projectEditorSettings); }) .catch(error => { - if (error && error.code === 'ENOENT') { - return; - } console.warn( 'Unable to read project editor settings. Opening the project without them.', error ); - }) - ) + }); + }) .then(() => { return { content: object }; }); From 16b04d65cb3df8e9183da7aa976517c0e8b64db1 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Tue, 12 May 2026 21:34:03 +0200 Subject: [PATCH 07/17] Try master commit libGD builds before master latest --- newIDE/app/scripts/import-libGD.js | 52 ++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/newIDE/app/scripts/import-libGD.js b/newIDE/app/scripts/import-libGD.js index 2accaaeac2f9..e5279b1e7d51 100644 --- a/newIDE/app/scripts/import-libGD.js +++ b/newIDE/app/scripts/import-libGD.js @@ -66,17 +66,28 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { return branch; }; + const getHashFromGitRef = gitRef => { + const hashShellString = shell.exec(`git rev-parse "${gitRef}"`, { + silent: true, + }); + const hash = (hashShellString.stdout || 'unknown-hash').trim(); + + if (hashShellString.stderr || hashShellString.code) { + shell.echo(`⚠️ Can't find the hash of the associated commit.`); + return null; + } + + return hash; + }; + // Try to download libGD.js from a specific commit on the current branch const downloadCommitLibGdJs = (branch, gitRef) => new Promise((resolve, reject) => { shell.echo(`ℹ️ Trying to download libGD.js for ${gitRef}.`); - var hashShellString = shell.exec(`git rev-parse "${gitRef}"`, { - silent: true, - }); - const hash = (hashShellString.stdout || 'unknown-hash').trim(); + const hash = getHashFromGitRef(gitRef); const branch = getBranchFromGitRef(gitRef); - if (hashShellString.stderr || hashShellString.code || !branch) { + if (!hash || !branch) { shell.echo( `⚠️ Can't find the hash or branch of the associated commit.` ); @@ -91,6 +102,25 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { ); }); + const downloadMasterCommitLibGdJs = gitRef => + new Promise((resolve, reject) => { + shell.echo( + `ℹ️ Trying to download libGD.js for ${gitRef} from master.` + ); + + const hash = getHashFromGitRef(gitRef); + if (!hash) { + reject(); + return; + } + + resolve( + downloadLibGdJs( + `https://s3.amazonaws.com/gdevelop-gdevelop.js/master/commit/${hash}` + ) + ); + }); + // Try to download libGD.js from the latest version built for master branch. const downloadBranchLatestLibGdJs = branchName => { shell.echo( @@ -151,10 +181,14 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { }; const branch = getBranchFromGitRef('HEAD'); + const downloadCommitLibGdJsWithMasterFallback = gitRef => + downloadCommitLibGdJs(branch, gitRef).catch(() => + downloadMasterCommitLibGdJs(gitRef) + ); // Try to download the latest libGD.js, fallback to previous or master ones // if not found (including different parents, for handling of merge commits). - downloadCommitLibGdJs(branch, 'HEAD').then(onLibGdJsDownloaded, () => { + downloadCommitLibGdJsWithMasterFallback('HEAD').then(onLibGdJsDownloaded, () => { // Force the exact version of GDevelop.js to be downloaded for AppVeyor - because // this means we build the app and we don't want to risk mismatch (Core C++ not up to date // with the IDE JavaScript). @@ -168,9 +202,9 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { shell.exit(1); } - downloadCommitLibGdJs(branch, 'HEAD~1').then(onLibGdJsDownloaded, () => - downloadCommitLibGdJs(branch, 'HEAD~2').then(onLibGdJsDownloaded, () => - downloadCommitLibGdJs(branch, 'HEAD~3').then(onLibGdJsDownloaded, () => + downloadCommitLibGdJsWithMasterFallback('HEAD~1').then(onLibGdJsDownloaded, () => + downloadCommitLibGdJsWithMasterFallback('HEAD~2').then(onLibGdJsDownloaded, () => + downloadCommitLibGdJsWithMasterFallback('HEAD~3').then(onLibGdJsDownloaded, () => downloadBranchLatestLibGdJs(branch).then(onLibGdJsDownloaded, () => downloadBranchLatestLibGdJs('master').then( onLibGdJsDownloaded, From eb3240f52395f78f11921e97b01ffa08d367ca0b Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Tue, 12 May 2026 21:37:46 +0200 Subject: [PATCH 08/17] Keep libGD fallback chain formatted --- newIDE/app/scripts/import-libGD.js | 89 +++++++++++++++--------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/newIDE/app/scripts/import-libGD.js b/newIDE/app/scripts/import-libGD.js index e5279b1e7d51..7c705358265e 100644 --- a/newIDE/app/scripts/import-libGD.js +++ b/newIDE/app/scripts/import-libGD.js @@ -80,8 +80,8 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { return hash; }; - // Try to download libGD.js from a specific commit on the current branch - const downloadCommitLibGdJs = (branch, gitRef) => + // Try to download libGD.js from a specific commit on the inferred branch. + const downloadCommitLibGdJs = gitRef => new Promise((resolve, reject) => { shell.echo(`ℹ️ Trying to download libGD.js for ${gitRef}.`); @@ -182,51 +182,54 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { const branch = getBranchFromGitRef('HEAD'); const downloadCommitLibGdJsWithMasterFallback = gitRef => - downloadCommitLibGdJs(branch, gitRef).catch(() => + downloadCommitLibGdJs(gitRef).catch(() => downloadMasterCommitLibGdJs(gitRef) ); + const tryDownloadInOrder = downloaders => + downloaders.reduce( + (previousDownload, download) => previousDownload.catch(() => download()), + Promise.reject() + ); // Try to download the latest libGD.js, fallback to previous or master ones // if not found (including different parents, for handling of merge commits). - downloadCommitLibGdJsWithMasterFallback('HEAD').then(onLibGdJsDownloaded, () => { - // Force the exact version of GDevelop.js to be downloaded for AppVeyor - because - // this means we build the app and we don't want to risk mismatch (Core C++ not up to date - // with the IDE JavaScript). - if (process.env.APPVEYOR || process.env.REQUIRES_EXACT_LIBGD_JS_VERSION) { - shell.echo( - `❌ Can't download the exact required version of libGD.js - check it was built by CircleCI before running this CI.` - ); - shell.echo( - `ℹ️ See the pipeline on https://app.circleci.com/pipelines/github/4ian/GDevelop.` - ); - shell.exit(1); - } + downloadCommitLibGdJsWithMasterFallback('HEAD').then( + onLibGdJsDownloaded, + () => { + // Force the exact version of GDevelop.js to be downloaded for AppVeyor - because + // this means we build the app and we don't want to risk mismatch (Core C++ not up to date + // with the IDE JavaScript). + if (process.env.APPVEYOR || process.env.REQUIRES_EXACT_LIBGD_JS_VERSION) { + shell.echo( + `❌ Can't download the exact required version of libGD.js - check it was built by CircleCI before running this CI.` + ); + shell.echo( + `ℹ️ See the pipeline on https://app.circleci.com/pipelines/github/4ian/GDevelop.` + ); + shell.exit(1); + } - downloadCommitLibGdJsWithMasterFallback('HEAD~1').then(onLibGdJsDownloaded, () => - downloadCommitLibGdJsWithMasterFallback('HEAD~2').then(onLibGdJsDownloaded, () => - downloadCommitLibGdJsWithMasterFallback('HEAD~3').then(onLibGdJsDownloaded, () => - downloadBranchLatestLibGdJs(branch).then(onLibGdJsDownloaded, () => - downloadBranchLatestLibGdJs('master').then( - onLibGdJsDownloaded, - () => { - if (alreadyHasLibGdJs) { - shell.echo( - `ℹ️ Can't download any version of libGD.js, assuming you can go ahead with the existing one.` - ); - shell.exit(0); - return; - } else { - shell.echo( - `❌ Can't download any version of libGD.js, please check your internet connection.` - ); - shell.exit(1); - return; - } - } - ) - ) - ) - ) - ); - }); + tryDownloadInOrder([ + () => downloadCommitLibGdJsWithMasterFallback('HEAD~1'), + () => downloadCommitLibGdJsWithMasterFallback('HEAD~2'), + () => downloadCommitLibGdJsWithMasterFallback('HEAD~3'), + () => downloadBranchLatestLibGdJs(branch), + () => downloadBranchLatestLibGdJs('master'), + ]).then(onLibGdJsDownloaded, () => { + if (alreadyHasLibGdJs) { + shell.echo( + `ℹ️ Can't download any version of libGD.js, assuming you can go ahead with the existing one.` + ); + shell.exit(0); + return; + } else { + shell.echo( + `❌ Can't download any version of libGD.js, please check your internet connection.` + ); + shell.exit(1); + return; + } + }); + } + ); } From 823b4728b75fbe42cda95c5a6e2e55eb9fd18039 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sat, 16 May 2026 15:43:16 +0200 Subject: [PATCH 09/17] Store editor settings per project file --- .../LocalEditorSettingsStorage.js | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js index d26b4dc298f4..a7bb202dfbeb 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -4,7 +4,7 @@ import optionalRequire from '../../Utils/OptionalRequire'; const path = optionalRequire('path'); export const editorSettingsDirectoryName = '.gdevelop'; -export const editorSettingsFileName = 'editor-settings.json'; +export const editorSettingsFileSuffix = '.editor-settings.json'; export type ProjectEditorSettings = {| layoutSettings: { [layoutName: string]: Object }, @@ -14,17 +14,15 @@ export type ProjectEditorSettings = {| }, |}; -export const getProjectEditorSettingsFilePath = ( - projectPath: string -): string => { +export const getProjectEditorSettingsFilePath = (filePath: string): string => { if (!path) { throw new Error('Filesystem paths are not supported.'); } return path.join( - projectPath, + path.dirname(filePath), editorSettingsDirectoryName, - editorSettingsFileName + path.basename(filePath, path.extname(filePath)) + editorSettingsFileSuffix ); }; @@ -35,7 +33,7 @@ const makeProjectEditorSettings = (): ProjectEditorSettings => ({ }); const hasOwn = (object: Object, propertyName: string): boolean => - Object.keys(object).indexOf(propertyName) !== -1; + Object.prototype.hasOwnProperty.call(object, propertyName); const isEmpty = (object: Object): boolean => Object.keys(object).length === 0; @@ -70,8 +68,9 @@ export const extractProjectEditorSettings = ( typeof externalLayout.name === 'string' && hasOwn(externalLayout, 'editionSettings') ) { - projectEditorSettings.externalLayoutSettings[externalLayout.name] = - externalLayout.editionSettings; + projectEditorSettings.externalLayoutSettings[ + externalLayout.name + ] = externalLayout.editionSettings; delete externalLayout.editionSettings; } }); @@ -88,7 +87,10 @@ export const extractProjectEditorSettings = ( } extension.eventsBasedObjects.forEach(eventsBasedObject => { - if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { + if ( + !eventsBasedObject || + typeof eventsBasedObject.name !== 'string' + ) { return; } @@ -168,7 +170,9 @@ export const applyProjectEditorSettings = ( }); } - const { eventsBasedObjectVariantSettings } = projectEditorSettings; + const { + eventsBasedObjectVariantSettings, + } = projectEditorSettings; if ( eventsBasedObjectVariantSettings && Array.isArray(serializedProjectObject.eventsFunctionsExtensions) @@ -183,7 +187,10 @@ export const applyProjectEditorSettings = ( } extension.eventsBasedObjects.forEach(eventsBasedObject => { - if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { + if ( + !eventsBasedObject || + typeof eventsBasedObject.name !== 'string' + ) { return; } From d484fe0ef20d9f724ee562cb3a883974ed7e437f Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sat, 16 May 2026 15:43:41 +0200 Subject: [PATCH 10/17] Cover per-project editor settings paths --- .../LocalEditorSettingsStorage.spec.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js index e6b72b8fa3ed..ed75af1578d8 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js @@ -2,9 +2,25 @@ import { applyProjectEditorSettings, extractProjectEditorSettings, + getProjectEditorSettingsFilePath, } from './LocalEditorSettingsStorage'; +// $FlowFixMe[cannot-resolve-module] +import path from 'path'; describe('LocalEditorSettingsStorage', () => { + it('stores settings separately for projects in the same folder', () => { + expect( + getProjectEditorSettingsFilePath('C:/Projects/game.json') + ).toBe( + path.join('C:/Projects', '.gdevelop', 'game.editor-settings.json') + ); + expect( + getProjectEditorSettingsFilePath('C:/Projects/prototype.json') + ).toBe( + path.join('C:/Projects', '.gdevelop', 'prototype.editor-settings.json') + ); + }); + it('extracts editor settings from project content and applies them back', () => { const serializedProjectObject: any = { layouts: [ From e975378a89a61d1f9f8a547e01f1a7e2ac5df40f Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sat, 16 May 2026 15:44:05 +0200 Subject: [PATCH 11/17] Read project-specific editor settings sidecars --- .../LocalProjectOpener.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js index f5df97ce9371..9fec75abf6fe 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectOpener.js @@ -39,25 +39,21 @@ export const onOpen = ( // of large game files. maxUnsplitDepth: 3, }) - .then(() => { - const projectEditorSettingsFilePath = getProjectEditorSettingsFilePath( - projectPath - ); - if (!fs.existsSync(projectEditorSettingsFilePath)) { - return; - } - - return readJSONFile(projectEditorSettingsFilePath) + .then(() => + readJSONFile(getProjectEditorSettingsFilePath(filePath)) .then(projectEditorSettings => { applyProjectEditorSettings(object, projectEditorSettings); }) .catch(error => { + if (error && error.code === 'ENOENT') { + return; + } console.warn( 'Unable to read project editor settings. Opening the project without them.', error ); - }); - }) + }) + ) .then(() => { return { content: object }; }); From c9e6f6a06604ed23683b34c453cae43fc21aeb26 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sat, 16 May 2026 15:45:29 +0200 Subject: [PATCH 12/17] Write project-specific editor settings sidecars --- .../LocalFileStorageProvider/LocalProjectWriter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js index 9824dd706347..cb29d1cb080f 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter.js @@ -143,10 +143,10 @@ const writeProjectFiles = async ({ if (projectEditorSettings) { await writeAndCheckFormattedJSONFile( projectEditorSettings, - getProjectEditorSettingsFilePath(projectPath) + getProjectEditorSettingsFilePath(filePath) ); } else { - await fs.remove(getProjectEditorSettingsFilePath(projectPath)); + await fs.remove(getProjectEditorSettingsFilePath(filePath)); } if (project.isFolderProject()) { From 9b61ae0b9be4ba33343743cae3eec515a1a04bfd Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sun, 17 May 2026 17:21:29 +0200 Subject: [PATCH 13/17] Keep editor settings helper Flow-safe --- .../LocalFileStorageProvider/LocalEditorSettingsStorage.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js index a7bb202dfbeb..9253e6febb24 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -33,7 +33,7 @@ const makeProjectEditorSettings = (): ProjectEditorSettings => ({ }); const hasOwn = (object: Object, propertyName: string): boolean => - Object.prototype.hasOwnProperty.call(object, propertyName); + Object.keys(object).indexOf(propertyName) !== -1; const isEmpty = (object: Object): boolean => Object.keys(object).length === 0; @@ -170,9 +170,7 @@ export const applyProjectEditorSettings = ( }); } - const { - eventsBasedObjectVariantSettings, - } = projectEditorSettings; + const { eventsBasedObjectVariantSettings } = projectEditorSettings; if ( eventsBasedObjectVariantSettings && Array.isArray(serializedProjectObject.eventsFunctionsExtensions) From e127c472b0709f0fef25e06caabe987032666389 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sun, 17 May 2026 17:21:55 +0200 Subject: [PATCH 14/17] Format editor settings path coverage --- .../LocalEditorSettingsStorage.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js index ed75af1578d8..f15c3244e7ef 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js @@ -9,9 +9,7 @@ import path from 'path'; describe('LocalEditorSettingsStorage', () => { it('stores settings separately for projects in the same folder', () => { - expect( - getProjectEditorSettingsFilePath('C:/Projects/game.json') - ).toBe( + expect(getProjectEditorSettingsFilePath('C:/Projects/game.json')).toBe( path.join('C:/Projects', '.gdevelop', 'game.editor-settings.json') ); expect( From 0fd57d63651541397c617e2599c861c22bf70585 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sun, 17 May 2026 20:25:04 +0200 Subject: [PATCH 15/17] Format local editor settings storage --- .../LocalFileStorageProvider/LocalEditorSettingsStorage.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js index 9253e6febb24..fa325436f3b2 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -68,9 +68,8 @@ export const extractProjectEditorSettings = ( typeof externalLayout.name === 'string' && hasOwn(externalLayout, 'editionSettings') ) { - projectEditorSettings.externalLayoutSettings[ - externalLayout.name - ] = externalLayout.editionSettings; + projectEditorSettings.externalLayoutSettings[externalLayout.name] = + externalLayout.editionSettings; delete externalLayout.editionSettings; } }); From b46aa9f43392f2c68857a4528d3b744d0e769900 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sun, 17 May 2026 20:25:31 +0200 Subject: [PATCH 16/17] Format local editor settings path coverage --- .../LocalEditorSettingsStorage.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js index f15c3244e7ef..b8998306363c 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.spec.js @@ -12,9 +12,7 @@ describe('LocalEditorSettingsStorage', () => { expect(getProjectEditorSettingsFilePath('C:/Projects/game.json')).toBe( path.join('C:/Projects', '.gdevelop', 'game.editor-settings.json') ); - expect( - getProjectEditorSettingsFilePath('C:/Projects/prototype.json') - ).toBe( + expect(getProjectEditorSettingsFilePath('C:/Projects/prototype.json')).toBe( path.join('C:/Projects', '.gdevelop', 'prototype.editor-settings.json') ); }); From 38503477361d3b0422b7f68141438e70610d68e4 Mon Sep 17 00:00:00 2001 From: bodhibuurstede-sys Date: Sun, 17 May 2026 23:20:38 +0200 Subject: [PATCH 17/17] Format local editor settings control flow --- .../LocalEditorSettingsStorage.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js index fa325436f3b2..09d77b2a832a 100644 --- a/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js +++ b/newIDE/app/src/ProjectsStorage/LocalFileStorageProvider/LocalEditorSettingsStorage.js @@ -86,10 +86,7 @@ export const extractProjectEditorSettings = ( } extension.eventsBasedObjects.forEach(eventsBasedObject => { - if ( - !eventsBasedObject || - typeof eventsBasedObject.name !== 'string' - ) { + if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { return; } @@ -184,10 +181,7 @@ export const applyProjectEditorSettings = ( } extension.eventsBasedObjects.forEach(eventsBasedObject => { - if ( - !eventsBasedObject || - typeof eventsBasedObject.name !== 'string' - ) { + if (!eventsBasedObject || typeof eventsBasedObject.name !== 'string') { return; }