Compare commits

...

12 Commits

Author SHA1 Message Date
Florian Rival
fa2371274d Add support for external layouts and reload when a change is made in the runtime 2025-01-02 00:10:45 +01:00
Florian Rival
0aea8dfa0f WIP: refactoring and prepare for handling external layout in-game edition 2024-12-31 17:07:21 +01:00
Florian Rival
81ca18098d Fix text object pre-rendering update 2024-12-31 16:04:11 +01:00
Florian Rival
b6e44a022f Add a note 2024-12-30 20:06:38 +01:00
Florian Rival
1a8eee2477 Remove game.resumed/paused in favor of status 2024-12-30 20:00:28 +01:00
Florian Rival
d0ef92da03 Fix flow and formatting 2024-12-30 19:25:43 +01:00
Florian Rival
9c98cb3b3b Relaunch an (in-game edition) preview when a change is made in GDJS Runtime 2024-12-30 18:56:09 +01:00
Florian Rival
3681542056 Robustify in game preview state management in the editor 2024-12-30 15:38:21 +01:00
Florian Rival
7c0bf135d7 WIP: Support for rendering while paused for in game edition 2024-12-30 12:32:05 +01:00
Florian Rival
9a31dd046c WIP: robustify scene change and initial scene for in game edition 2024-12-29 17:23:20 +01:00
Florian Rival
74401a1f9c WIP: Add scene change 2024-12-29 15:50:50 +01:00
Florian Rival
cedc6ea3e9 WIP 2024-12-29 15:50:50 +01:00
42 changed files with 1100 additions and 460 deletions

View File

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

View File

@@ -320,7 +320,7 @@ namespace gdjs {
return this._renderer.getRendererObject();
}
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
this._renderer.ensureUpToDate();
}

View File

@@ -216,17 +216,30 @@ bool ExporterHelper::ExportProjectForPixiPreview(
// Strip the project (*after* generating events as the events may use stripped
// things (objects groups...))
gd::ProjectStripper::StripProjectForExport(exportedProject);
exportedProject.SetFirstLayout(options.layoutName);
previousTime = LogTimeSpent("Data stripping", previousTime);
// Create the setup options passed to the gdjs.RuntimeGame
gd::SerializerElement runtimeGameOptions;
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
if (!options.externalLayoutName.empty()) {
runtimeGameOptions.AddChild("injectExternalLayout")
.SetValue(options.externalLayoutName);
auto &initialRuntimeGameStatus =
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
initialRuntimeGameStatus.AddChild("sceneName")
.SetStringValue(options.layoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
}
if (!options.externalLayoutName.empty()) {
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
.SetValue(options.externalLayoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
.SetBoolValue(true);
}
}
runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("nativeMobileApp")

View File

@@ -45,6 +45,7 @@ struct PreviewExportOptions {
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
isInGameEdition(false),
nonRuntimeScriptsCacheBurst(0),
fallbackAuthorId(""),
fallbackAuthorUsername(""),
@@ -169,6 +170,14 @@ struct PreviewExportOptions {
return *this;
}
/**
* \brief Set if the export is made for being edited in the editor.
*/
PreviewExportOptions &SetIsInGameEdition(bool enable) {
isInGameEdition = enable;
return *this;
}
/**
* \brief If set to a non zero value, the exported script URLs will have an
* extra search parameter added (with the given value) to ensure browser cache
@@ -291,6 +300,7 @@ struct PreviewExportOptions {
bool projectDataOnlyExport;
bool fullLoadingScreen;
bool isDevelopmentEnvironment;
bool isInGameEdition;
unsigned int nonRuntimeScriptsCacheBurst;
gd::String electronRemoteRequirePath;
gd::String gdevelopResourceToken;

View File

@@ -245,6 +245,8 @@ namespace gdjs {
that.sendRuntimeGameDump();
} else if (data.command === 'refresh') {
that.sendRuntimeGameDump();
} else if (data.command === 'getStatus') {
that.sendRuntimeGameStatus();
} else if (data.command === 'set') {
that.set(data.path, data.newValue);
} else if (data.command === 'call') {
@@ -263,7 +265,84 @@ namespace gdjs {
} else if (data.command === 'hotReload') {
that._hotReloader.hotReload().then((logs) => {
that.sendHotReloaderLogs(logs);
// TODO: if fatal error, should probably reload. The editor should handle this
// as it knows the current scene to show.
});
} else if (data.command === 'switchForInGameEdition') {
if (!this._runtimegame.isInGameEdition()) return;
const sceneName = data.sceneName || null;
const externalLayoutName = data.externalLayoutName || null;
if (!sceneName) {
logger.warn('No scene name specified, switchForInGameEdition aborted');
return;
}
const runtimeGameOptions = this._runtimegame.getAdditionalOptions();
if (runtimeGameOptions.initialRuntimeGameStatus) {
// Skip changing the scene if we're already on the state that is being requested.
if (
runtimeGameOptions.initialRuntimeGameStatus.sceneName ===
sceneName &&
runtimeGameOptions.initialRuntimeGameStatus
.injectedExternalLayoutName === externalLayoutName
) {
return;
}
}
runtimeGame
.getSceneStack()
.replace({
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene: !!externalLayoutName,
clear: true,
});
// Update initialRuntimeGameStatus so that a hard reload
// will come back to the same state, and so that we can check later
// if the game is already on the state that is being requested.
runtimeGameOptions.initialRuntimeGameStatus = {
isPaused: runtimeGame.isPaused(),
isInGameEdition: runtimeGame.isInGameEdition(),
sceneName: sceneName,
injectedExternalLayoutName: externalLayoutName,
skipCreatingInstancesFromScene: !!externalLayoutName,
};
} else if (data.command === 'updateInstances') {
// TODO: do an update/partial hot reload of the instances
} else if (data.command === 'hardReload') {
// This usually means that the preview was modified so much that an entire reload
// is needed, or that the runtime itself could have been modified.
try {
const reloadUrl = new URL(location.href);
// Construct the initial status to be restored.
const initialRuntimeGameStatus = this._runtimegame.getAdditionalOptions()
.initialRuntimeGameStatus;
const runtimeGameStatus: RuntimeGameStatus = {
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
sceneName: initialRuntimeGameStatus?.sceneName || null,
injectedExternalLayoutName:
initialRuntimeGameStatus?.injectedExternalLayoutName || null,
skipCreatingInstancesFromScene:
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
};
reloadUrl.searchParams.set(
'runtimeGameStatus',
JSON.stringify(runtimeGameStatus)
);
location.replace(reloadUrl);
} catch (error) {
logger.error(
'Could not reload the game with the new initial status',
error
);
location.reload();
}
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
@@ -434,6 +513,20 @@ namespace gdjs {
return true;
}
sendRuntimeGameStatus(): void {
const currentScene = this._runtimegame.getSceneStack().getCurrentScene();
this._sendMessage(
circularSafeStringify({
command: 'status',
payload: {
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
sceneName: currentScene ? currentScene.getName() : null,
},
})
);
}
/**
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
*/
@@ -543,26 +636,11 @@ namespace gdjs {
);
}
/**
* Callback called when the game is paused.
*/
sendGamePaused(): void {
sendInstancesUpdated(runtimeObjects: gdjs.RuntimeObject[]): void {
this._sendMessage(
circularSafeStringify({
command: 'game.paused',
payload: null,
})
);
}
/**
* Callback called when the game is resumed.
*/
sendGameResumed(): void {
this._sendMessage(
circularSafeStringify({
command: 'game.resumed',
payload: null,
command: 'instances.updated',
payload: 'TODO',
})
);
}

View File

@@ -144,8 +144,9 @@ namespace gdjs {
});
}
hotReload(): Promise<HotReloaderLog[]> {
async hotReload(): Promise<HotReloaderLog[]> {
logger.info('Hot reload started');
const wasPaused = this._runtimeGame.isPaused();
this._runtimeGame.pause(true);
this._logs = [];
@@ -168,62 +169,59 @@ namespace gdjs {
}
// Reload projectData and runtimeGameOptions stored by convention in data.js:
return this._reloadScript('data.js').then(() => {
const newProjectData: ProjectData = gdjs.projectData;
await this._reloadScript('data.js');
const newRuntimeGameOptions: RuntimeGameOptions =
gdjs.runtimeGameOptions;
const newProjectData: ProjectData = gdjs.projectData;
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
const newRuntimeGameOptions: RuntimeGameOptions = gdjs.runtimeGameOptions;
// Reload the changed scripts, which will have the side effects of re-running
// the new scripts, potentially replacing the code of the free functions from
// extensions (which is fine) and registering updated behaviors (which will
// need to be re-instantiated in runtime objects).
return this.reloadScriptFiles(
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
// Reload the changed scripts, which will have the side effects of re-running
// the new scripts, potentially replacing the code of the free functions from
// extensions (which is fine) and registering updated behaviors (which will
// need to be re-instantiated in runtime objects).
try {
await this.reloadScriptFiles(
newProjectData,
oldScriptFiles,
newScriptFiles,
projectDataOnlyExport
)
.then(() => {
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
oldBehaviorConstructors,
gdjs.behaviorsTypes.items
);
return this._hotReloadRuntimeGame(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
this._runtimeGame
);
})
.catch((error) => {
const errorTarget = error.target;
if (errorTarget instanceof HTMLScriptElement) {
this._logs.push({
kind: 'fatal',
message: 'Unable to reload script: ' + errorTarget.src,
});
} else {
this._logs.push({
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading: ' +
error.message,
});
}
})
.then(() => {
logger.info(
'Hot reload finished with logs:',
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
);
this._runtimeGame.pause(false);
return this._logs;
);
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
oldBehaviorConstructors,
gdjs.behaviorsTypes.items
);
await this._hotReloadRuntimeGame(
oldProjectData,
newProjectData,
changedRuntimeBehaviors,
this._runtimeGame
);
} catch (error) {
const errorTarget = error.target;
if (errorTarget instanceof HTMLScriptElement) {
this._logs.push({
kind: 'fatal',
message: 'Unable to reload script: ' + errorTarget.src,
});
});
} else {
this._logs.push({
kind: 'fatal',
message:
'Unexpected error happened while hot-reloading: ' + error.message,
});
}
}
logger.info(
'Hot reload finished with logs:',
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
);
this._runtimeGame.pause(wasPaused);
return this._logs;
}
_computeChangedRuntimeBehaviors(

View File

@@ -41,14 +41,54 @@ namespace gdjs {
return supportedCompressionMethods;
};
/**
* The desired status of the game, used for previews or in-game edition.
* Either stored in the options generated by the preview or in the URL
* in case of a hard reload.
*/
type RuntimeGameStatus = {
isPaused: boolean;
isInGameEdition: boolean;
sceneName: string | null;
injectedExternalLayoutName: string | null;
skipCreatingInstancesFromScene: boolean;
};
/**
* Read the desired status of the game from the URL. Only useful for previews
* when hard reloaded.
*/
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
try {
const url = new URL(location.href);
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
if (!runtimeGameStatus) return null;
const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
return {
isPaused: !!parsedRuntimeGameStatus.isPaused,
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
sceneName: '' + parsedRuntimeGameStatus.sceneName,
injectedExternalLayoutName:
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
skipCreatingInstancesFromScene: !!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
};
} catch (e) {
return null;
}
};
/** Options given to the game at startup. */
export type RuntimeGameOptions = {
/** if true, force fullscreen. */
forceFullscreen?: boolean;
/** if true, game is run as a preview launched from an editor. */
isPreview?: boolean;
/** The name of the external layout to create in the scene at position 0;0. */
injectExternalLayout?: string;
/** if set, the status of the game to be restored. */
initialRuntimeGameStatus?: RuntimeGameStatus;
/** Script files, used for hot-reloading. */
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without events. */
@@ -169,10 +209,6 @@ namespace gdjs {
//Inputs :
_inputManager: InputManager;
/**
* Allow to specify an external layout to insert in the first scene.
*/
_injectExternalLayout: any;
_options: RuntimeGameOptions;
/**
@@ -187,6 +223,7 @@ namespace gdjs {
_sessionMetricsInitialized: boolean = false;
_disableMetrics: boolean = false;
_isPreview: boolean;
_isInGameEdition: boolean;
/**
* The capture manager, used to manage captures (screenshots, videos, etc...).
@@ -202,6 +239,19 @@ namespace gdjs {
*/
constructor(data: ProjectData, options?: RuntimeGameOptions) {
this._options = options || {};
this._isPreview = this._options.isPreview || false;
if (this._isPreview) {
// Check if we need to restore the state from the URL, which is used
// when a preview is hard reloaded (search for `hardReload`).
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
if (runtimeGameStatusFromUrl) {
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
}
}
this._isInGameEdition =
this._options.initialRuntimeGameStatus?.isInGameEdition || false;
this._variables = new gdjs.VariablesContainer(data.variables);
this._variablesByExtensionName = new Map<
string,
@@ -250,7 +300,6 @@ namespace gdjs {
);
this._sceneStack = new gdjs.SceneStack(this);
this._inputManager = new gdjs.InputManager();
this._injectExternalLayout = this._options.injectExternalLayout || '';
this._debuggerClient = gdjs.DebuggerClient
? new gdjs.DebuggerClient(this)
: null;
@@ -260,7 +309,6 @@ namespace gdjs {
this._options.captureOptions || {}
)
: null;
this._isPreview = this._options.isPreview || false;
this._sessionId = null;
this._playerId = null;
@@ -710,8 +758,7 @@ namespace gdjs {
this._paused = enable;
if (this._debuggerClient) {
if (this._paused) this._debuggerClient.sendGamePaused();
else this._debuggerClient.sendGameResumed();
this._debuggerClient.sendRuntimeGameStatus();
}
}
@@ -872,11 +919,16 @@ namespace gdjs {
await loadAssets(onProgress);
await loadingScreen.unload();
this.pause(false);
if (!this._isInGameEdition) {
this.pause(false);
}
}
private _getFirstSceneName(): string {
const firstSceneName = this._data.firstLayout;
const firstSceneName =
this._options.initialRuntimeGameStatus?.sceneName ||
this._data.firstLayout;
return this.hasScene(firstSceneName)
? firstSceneName
: // There is always at least a scene
@@ -896,10 +948,15 @@ namespace gdjs {
this._forceGameResolutionUpdate();
// Load the first scene
this._sceneStack.push(
this._getFirstSceneName(),
this._injectExternalLayout
);
this._sceneStack.push({
sceneName: this._getFirstSceneName(),
externalLayoutName:
this._options.initialRuntimeGameStatus
?.injectedExternalLayoutName || undefined,
skipCreatingInstancesFromScene:
this._options.initialRuntimeGameStatus
?.skipCreatingInstancesFromScene || false,
});
this._watermark.displayAtStartup();
//Uncomment to profile the first x frames of the game.
@@ -917,11 +974,25 @@ namespace gdjs {
this._setupGameVisibilityEvents();
// The standard game loop
let lastFrameSceneName: string | null = null;
let accumulatedElapsedTime = 0;
this._hasJustResumed = false;
this._renderer.startGameLoop((lastCallElapsedTime) => {
try {
if (this._paused) {
if (this._debuggerClient) {
// Watch the scene name to automatically update debugger when a scene is changed.
const currentScene = this.getSceneStack().getCurrentScene();
if (
currentScene &&
currentScene.getName() !== lastFrameSceneName
) {
lastFrameSceneName = currentScene.getName();
this._debuggerClient.sendRuntimeGameStatus();
}
}
if (this._paused && !this._isInGameEdition) {
// The game is paused, but not being edited, so we entirely skip any logic.
return true;
}
@@ -946,13 +1017,20 @@ namespace gdjs {
this._notifyScenesForGameResolutionResize = false;
}
// Render and step the scene.
if (this._sceneStack.step(elapsedTime)) {
this.getInputManager().onFrameEnded();
this._hasJustResumed = false;
if (this._paused && this._isInGameEdition) {
// The game is paused for edition: the game loop continues to run,
// but the game logic is not executed.
this._sceneStack.renderWithoutStep();
return true;
} else {
// Render and step the scene.
if (this._sceneStack.step(elapsedTime)) {
this.getInputManager().onFrameEnded();
this._hasJustResumed = false;
return true;
}
return false;
}
return false;
} catch (e) {
if (this._debuggerClient)
this._debuggerClient.onUncaughtException(e);
@@ -1273,6 +1351,22 @@ namespace gdjs {
return this._isPreview;
}
/**
* Check if the game loop is paused, for debugging/edition purposes.
* @returns true if the current game is paused
*/
isPaused(): boolean {
return this._paused;
}
/**
* Check if the game should display in-game edition tools or not.
* @returns true if the current game is being edited.
*/
isInGameEdition(): boolean {
return this._isInGameEdition;
}
/**
* Check if the game should call GDevelop development APIs or not.
*

View File

@@ -121,10 +121,15 @@ namespace gdjs {
/**
* Load the runtime scene from the given scene.
* @param sceneData An object containing the scene data.
*
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
* @param options Options to change what is loaded.
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
*/
loadFromScene(sceneAndExtensionsData: SceneAndExtensionsData | null) {
loadFromScene(
sceneAndExtensionsData: SceneAndExtensionsData | null,
options?: { skipCreatingInstances?: boolean }
) {
if (!sceneAndExtensionsData) {
logger.error('loadFromScene was called without a scene');
return;
@@ -184,14 +189,16 @@ namespace gdjs {
}
//Create initial instances of objects
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true
);
if (!options || !options.skipCreatingInstances) {
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true
);
}
// Set up the default z order (for objects created from events)
this._setLayerDefaultZOrders();
@@ -358,7 +365,7 @@ namespace gdjs {
}
/**
* Step and render the scene.
* Step (execute the game logic) and render the scene.
* @param elapsedTime In milliseconds
* @return true if the game loop should continue, false if a scene change/push/pop
* or a game stop was requested.
@@ -418,6 +425,23 @@ namespace gdjs {
if (this._profiler) {
this._profiler.end('callbacks and extensions (post-events)');
}
this.render();
this._isJustResumed = false;
if (this._profiler) {
this._profiler.end('render');
}
if (this._profiler) {
this._profiler.endFrame();
}
return !!this.getRequestedChange();
}
/**
* Render the scene (but do not execute the game logic).
*/
render() {
if (this._profiler) {
this._profiler.begin('objects (pre-render, effects update)');
}
@@ -447,21 +471,6 @@ namespace gdjs {
);
}
this._isJustResumed = false;
this.render();
if (this._profiler) {
this._profiler.end('render');
}
if (this._profiler) {
this._profiler.endFrame();
}
return !!this.getRequestedChange();
}
/**
* Render the PIXI container associated to the runtimeScene.
*/
render() {
this._renderer.render();
}

View File

@@ -2,6 +2,16 @@ namespace gdjs {
const logger = new gdjs.Logger('Scene stack');
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
interface PushSceneOptions {
sceneName: string;
externalLayoutName?: string;
skipCreatingInstancesFromScene?: boolean;
};
interface ReplaceSceneOptions extends PushSceneOptions {
clear: boolean;
};
/**
* Hold the stack of scenes ({@link gdjs.RuntimeScene}) being played.
*/
@@ -113,15 +123,28 @@ namespace gdjs {
}
}
/**
* Pause the scene currently being played and start the new scene that is specified.
* If `externalLayoutName` is set, also instantiate the objects from this external layout.
* Pause the scene currently being played and start the new scene that is specified in `options.sceneName`.
* If `options.externalLayoutName` is set, also instantiate the objects from this external layout.
*
* @param options Contains the scene name and optional external layout name to instantiate.
* @param deprecatedExternalLayoutName Deprecated, use `options.externalLayoutName` instead.
*/
push(
newSceneName: string,
externalLayoutName?: string
options: PushSceneOptions | string,
deprecatedExternalLayoutName?: string
): gdjs.RuntimeScene | null {
this._throwIfDisposed();
console.log({options, deprecatedExternalLayoutName})
const sceneName =
typeof options === 'string' ? options : options.sceneName;
const skipCreatingInstancesFromScene =
typeof options === 'string' ? false : options.skipCreatingInstancesFromScene;
const externalLayoutName =
deprecatedExternalLayoutName ||
(typeof options === 'string' ? undefined : options.externalLayoutName);
// Tell the scene it's being paused
const currentScene = this._stack[this._stack.length - 1];
@@ -131,35 +154,43 @@ namespace gdjs {
// Avoid a risk of displaying an intermediate loading screen
// during 1 frame.
if (this._runtimeGame.areSceneAssetsReady(newSceneName)) {
return this._loadNewScene(newSceneName, externalLayoutName);
if (this._runtimeGame.areSceneAssetsReady(sceneName)) {
return this._loadNewScene({
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene,
});
}
this._isNextLayoutLoading = true;
this._runtimeGame.loadSceneAssets(newSceneName).then(() => {
this._loadNewScene(newSceneName);
this._runtimeGame.loadSceneAssets(sceneName).then(() => {
this._loadNewScene({
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene,
});
this._isNextLayoutLoading = false;
});
return null;
}
private _loadNewScene(
newSceneName: string,
externalLayoutName?: string
): gdjs.RuntimeScene {
private _loadNewScene(options: PushSceneOptions): gdjs.RuntimeScene {
this._throwIfDisposed();
// Load the new one
const newScene = new gdjs.RuntimeScene(this._runtimeGame);
newScene.loadFromScene(
this._runtimeGame.getSceneAndExtensionsData(newSceneName)
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
{
skipCreatingInstances: options.skipCreatingInstancesFromScene,
}
);
this._wasFirstSceneLoaded = true;
// Optionally create the objects from an external layout.
if (externalLayoutName) {
if (options.externalLayoutName) {
const externalLayoutData = this._runtimeGame.getExternalLayoutData(
externalLayoutName
options.externalLayoutName
);
if (externalLayoutData) {
newScene.createObjectsFrom(
@@ -177,10 +208,15 @@ namespace gdjs {
}
/**
* Start the specified scene, replacing the one currently being played.
* If `clear` is set to true, all running scenes are also removed from the stack of scenes.
* Start the scene in `options.sceneName`, replacing the one currently being played.
* If `options.clear` is set to true, all running scenes are also removed from the stack of scenes.
*
* @param options Contains the scene name and optional external layout name to instantiate.
* @param deprecatedClear Deprecated, use `options.clear` instead.
*/
replace(newSceneName: string, clear?: boolean): gdjs.RuntimeScene | null {
replace(options: ReplaceSceneOptions | string, deprecatedClear?: boolean): gdjs.RuntimeScene | null {
const clear = deprecatedClear || typeof options === 'string' ? false : options.clear;
this._throwIfDisposed();
if (!!clear) {
// Unload all the scenes
@@ -199,7 +235,7 @@ namespace gdjs {
}
}
}
return this.push(newSceneName);
return this.push(options);
}
/**

View File

@@ -3843,6 +3843,7 @@ interface PreviewExportOptions {
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
[Ref] PreviewExportOptions SetIsInGameEdition(boolean enable);
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);

View File

@@ -2850,6 +2850,7 @@ export class PreviewExportOptions extends EmscriptenObject {
setNativeMobileApp(enable: boolean): PreviewExportOptions;
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
setIsInGameEdition(enable: boolean): PreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;
setElectronRemoteRequirePath(electronRemoteRequirePath: string): PreviewExportOptions;
setGDevelopResourceToken(gdevelopResourceToken: string): PreviewExportOptions;

View File

@@ -13,6 +13,7 @@ declare class gdPreviewExportOptions {
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
setIsInGameEdition(enable: boolean): gdPreviewExportOptions;
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;

View File

@@ -51,18 +51,19 @@ export default class CommandManager implements CommandManagerInterface {
}
registerCommand = (commandName: CommandName, command: Command) => {
if (this._commands[commandName])
return console.warn(
`Tried to register command ${commandName}, but it is already registered.`
);
if (this._commands[commandName]) return;
// if (this._commands[commandName])
// return console.warn(
// `Tried to register command ${commandName}, but it is already registered.`
// );
this._commands[commandName] = command;
};
deregisterCommand = (commandName: CommandName) => {
if (!this._commands[commandName])
return console.warn(
`Tried to deregister command ${commandName}, but it is not registered.`
);
// if (!this._commands[commandName])
// return console.warn(
// `Tried to deregister command ${commandName}, but it is not registered.`
// );
delete this._commands[commandName];
};

View File

@@ -1,38 +1,62 @@
// @flow
import { t } from '@lingui/macro';
import * as React from 'react';
import { I18n } from '@lingui/react';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { type DebuggerId } from '../ExportAndShare/PreviewLauncher.flow';
import {
type DebuggerId,
type DebuggerStatus,
} from '../ExportAndShare/PreviewLauncher.flow';
type Props = {|
selectedId: DebuggerId,
debuggerIds: Array<DebuggerId>,
debuggerStatus: { [DebuggerId]: DebuggerStatus },
onChooseDebugger: DebuggerId => void,
|};
export default class DebuggerSelector extends React.Component<Props, void> {
render() {
const hasDebuggers = !!this.props.debuggerIds.length;
const debuggerIds = Object.keys(this.props.debuggerStatus);
const hasDebuggers = !!debuggerIds.length;
return (
<SelectField
fullWidth
value={hasDebuggers ? this.props.selectedId : 0}
onChange={(e, i, value) =>
this.props.onChooseDebugger(parseInt(value, 10) || 0)
}
disabled={!hasDebuggers}
>
{this.props.debuggerIds.map(id => (
<SelectOption value={id} key={id} label={t`Game preview #${id}`} />
))}
{!hasDebuggers && (
<SelectOption
value={0}
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
/>
<I18n>
{({ i18n }) => (
<SelectField
fullWidth
value={hasDebuggers ? this.props.selectedId : 0}
onChange={(e, i, value) =>
this.props.onChooseDebugger(parseInt(value, 10) || 0)
}
disabled={!hasDebuggers}
>
{debuggerIds.map(id => {
const status = this.props.debuggerStatus[+id];
const statusText = status.isPaused
? status.isInGameEdition
? t`Editing`
: t`Paused`
: status.isInGameEdition
? t`Playing (in-game edition)`
: t`Playing`;
return (
<SelectOption
value={+id}
key={id}
label={t`Game preview #${id} (${i18n._(statusText)})`}
/>
);
})}
{!hasDebuggers && (
<SelectOption
value={0}
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
/>
)}
</SelectField>
)}
</SelectField>
</I18n>
);
}
}

View File

@@ -14,6 +14,7 @@ import EmptyMessage from '../UI/EmptyMessage';
import {
type PreviewDebuggerServer,
type DebuggerId,
type DebuggerStatus,
} from '../ExportAndShare/PreviewLauncher.flow';
import { type Log, LogsManager } from './DebuggerConsole';
@@ -53,7 +54,7 @@ type State = {|
debuggerGameData: { [DebuggerId]: any },
profilerOutputs: { [DebuggerId]: ProfilerOutput },
profilingInProgress: { [DebuggerId]: boolean },
gameIsPaused: { [DebuggerId]: boolean },
debuggerStatus: { [DebuggerId]: DebuggerStatus },
selectedId: DebuggerId,
logs: { [DebuggerId]: Array<Log> },
|};
@@ -70,7 +71,7 @@ export default class Debugger extends React.Component<Props, State> {
debuggerGameData: {},
profilerOutputs: {},
profilingInProgress: {},
gameIsPaused: {},
debuggerStatus: {},
selectedId: 0,
logs: {},
};
@@ -79,18 +80,22 @@ export default class Debugger extends React.Component<Props, State> {
_debuggerLogs: Map<number, LogsManager> = new Map();
updateToolbar = () => {
const { selectedId, gameIsPaused } = this.state;
const { selectedId, debuggerStatus } = this.state;
const selectedDebuggerContents = this._debuggerContents[
this.state.selectedId
];
const isSelectedDebuggerPaused = debuggerStatus[selectedId]
? debuggerStatus[selectedId].isPaused
: false;
this.props.setToolbar(
<Toolbar
onPlay={() => this._play(this.state.selectedId)}
onPause={() => this._pause(this.state.selectedId)}
canPlay={this._hasSelectedDebugger() && gameIsPaused[selectedId]}
canPause={this._hasSelectedDebugger() && !gameIsPaused[selectedId]}
canPlay={this._hasSelectedDebugger() && isSelectedDebuggerPaused}
canPause={this._hasSelectedDebugger() && !isSelectedDebuggerPaused}
canOpenProfiler={this._hasSelectedDebugger()}
isProfilerShown={
!!selectedDebuggerContents &&
@@ -161,14 +166,14 @@ export default class Debugger extends React.Component<Props, State> {
debuggerGameData,
profilerOutputs,
profilingInProgress,
gameIsPaused,
debuggerStatus,
}) => {
// Remove any data bound to the instance that might have been stored.
// Otherwise this would be a memory leak.
if (debuggerGameData[id]) delete debuggerGameData[id];
if (profilerOutputs[id]) delete profilerOutputs[id];
if (profilingInProgress[id]) delete profilingInProgress[id];
if (gameIsPaused[id]) delete gameIsPaused[id];
if (debuggerStatus[id]) delete debuggerStatus[id];
return {
debuggerIds,
@@ -181,7 +186,7 @@ export default class Debugger extends React.Component<Props, State> {
debuggerGameData,
profilerOutputs,
profilingInProgress,
gameIsPaused,
debuggerStatus,
};
},
() => this.updateToolbar()
@@ -219,6 +224,11 @@ export default class Debugger extends React.Component<Props, State> {
this.setState({
unregisterDebuggerServerCallbacks: unregisterCallbacks,
});
// Fetch the status of each debugger client.
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
previewDebuggerServer.sendMessage(debuggerId, { command: 'getStatus' });
});
};
_handleMessage = (id: DebuggerId, data: any) => {
@@ -229,6 +239,16 @@ export default class Debugger extends React.Component<Props, State> {
[id]: data.payload,
},
});
} else if (data.command === 'status') {
this.setState(
state => ({
debuggerStatus: {
...state.debuggerStatus,
[id]: data.payload,
},
}),
() => this.updateToolbar()
);
} else if (data.command === 'profiler.output') {
this.setState({
profilerOutputs: {
@@ -244,20 +264,6 @@ export default class Debugger extends React.Component<Props, State> {
this.setState(state => ({
profilingInProgress: { ...state.profilingInProgress, [id]: false },
}));
} else if (data.command === 'game.resumed') {
this.setState(
state => ({
gameIsPaused: { ...state.gameIsPaused, [id]: false },
}),
() => this.updateToolbar()
);
} else if (data.command === 'game.paused') {
this.setState(
state => ({
gameIsPaused: { ...state.gameIsPaused, [id]: true },
}),
() => this.updateToolbar()
);
} else if (data.command === 'hotReloader.logs') {
// Nothing to do.
} else if (data.command === 'console.log') {
@@ -276,24 +282,14 @@ export default class Debugger extends React.Component<Props, State> {
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'play' });
this.setState(
state => ({
gameIsPaused: { ...state.gameIsPaused, [id]: false },
}),
() => this.updateToolbar()
);
// Pause status is transmitted by the game (using `status`).
};
_pause = (id: DebuggerId) => {
const { previewDebuggerServer } = this.props;
previewDebuggerServer.sendMessage(id, { command: 'pause' });
this.setState(
state => ({
gameIsPaused: { ...state.gameIsPaused, [id]: true },
}),
() => this.updateToolbar()
);
// Pause status is transmitted by the game (using `status`).
};
_refresh = (id: DebuggerId) => {
@@ -345,7 +341,7 @@ export default class Debugger extends React.Component<Props, State> {
debuggerServerError,
debuggerServerState,
selectedId,
debuggerIds,
debuggerStatus,
debuggerGameData,
profilerOutputs,
profilingInProgress,
@@ -375,7 +371,7 @@ export default class Debugger extends React.Component<Props, State> {
<Column expand noMargin>
<DebuggerSelector
selectedId={selectedId}
debuggerIds={debuggerIds}
debuggerStatus={debuggerStatus}
onChooseDebugger={id =>
this.setState(
{

View File

@@ -0,0 +1,115 @@
// @flow
import * as React from 'react';
import { type PreviewDebuggerServer } from '../ExportAndShare/PreviewLauncher.flow';
type AttachToPreviewOptions = {|
previewIndexHtmlLocation: string,
|};
type SwitchToSceneEditionOptions = {|
sceneName: string,
externalLayoutName?: string,
|};
let onAttachToPreview: null | (AttachToPreviewOptions => void) = null;
let onSwitchToSceneEdition: null | (SwitchToSceneEditionOptions => void) = null;
export const attachToPreview = ({
previewIndexHtmlLocation,
}: AttachToPreviewOptions) => {
if (!onAttachToPreview) throw new Error('No EmbeddedGameFrame registered.');
onAttachToPreview({ previewIndexHtmlLocation });
};
export const switchToSceneEdition = ({
sceneName,
externalLayoutName,
}: SwitchToSceneEditionOptions) => {
if (!onSwitchToSceneEdition)
throw new Error('No EmbeddedGameFrame registered.');
onSwitchToSceneEdition({ sceneName, externalLayoutName });
};
type Props = {|
previewDebuggerServer: PreviewDebuggerServer | null,
onLaunchPreviewForInGameEdition: ({|
sceneName: string,
externalLayoutName: ?string,
|}) => void,
|};
export const EmbeddedGameFrame = ({
previewDebuggerServer,
onLaunchPreviewForInGameEdition,
}: Props) => {
const [
previewIndexHtmlLocation,
setPreviewIndexHtmlLocation,
] = React.useState<string>('');
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
React.useEffect(
() => {
// TODO: use a real context for this?
onAttachToPreview = (options: AttachToPreviewOptions) => {
setPreviewIndexHtmlLocation(options.previewIndexHtmlLocation);
if (iframeRef.current) {
iframeRef.current.contentWindow.focus();
}
};
onSwitchToSceneEdition = (options: SwitchToSceneEditionOptions) => {
if (!previewDebuggerServer) return;
const { sceneName, externalLayoutName } = options;
if (!previewIndexHtmlLocation) {
console.info(
externalLayoutName
? `Launching in-game edition preview for external layout "${externalLayoutName}" (scene: "${sceneName}").`
: `Launching in-game edition preview for scene "${sceneName}".`
);
onLaunchPreviewForInGameEdition({ sceneName, externalLayoutName });
} else {
console.info(
externalLayoutName
? `Switching in-game edition previews to external layout "${externalLayoutName}" (scene: "${sceneName}").`
: `Switching in-game edition previews to scene "${sceneName}".`
);
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
previewDebuggerServer.sendMessage(debuggerId, {
command: 'switchForInGameEdition',
sceneName,
externalLayoutName,
});
});
}
};
},
[
previewDebuggerServer,
previewIndexHtmlLocation,
onLaunchPreviewForInGameEdition,
]
);
return (
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<iframe
ref={iframeRef}
title="Game Preview"
src={previewIndexHtmlLocation}
tabIndex={-1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: 'none',
}}
/>
</div>
</div>
);
};

View File

@@ -20,6 +20,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
import { isNativeMobileApp } from '../../../Utils/Platform';
import { getIDEVersionWithHash } from '../../../Version';
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
const gd: libGDevelop = global.gd;
type State = {|
@@ -158,6 +159,9 @@ export default class BrowserS3PreviewLauncher extends React.Component<
);
previewExportOptions.setLayoutName(layout.getName());
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
previewExportOptions.setIsInGameEdition(
previewOptions.isForInGameEdition
);
if (externalLayout) {
previewExportOptions.setExternalLayoutName(externalLayout.getName());
}
@@ -220,6 +224,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
// Upload any file that must be exported for the preview.
await browserS3FileSystem.uploadPendingObjects();
if (previewOptions.isForInGameEdition) {
attachToPreview({
previewIndexHtmlLocation: outputDir + '/index.html',
});
}
// Change the HTML file displayed by the preview window so that it starts loading
// the game.
previewWindows.forEach(

View File

@@ -22,6 +22,7 @@ import {
} from './LocalPreviewDebuggerServer';
import Window from '../../../Utils/Window';
import { getIDEVersionWithHash } from '../../../Version';
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const ipcRenderer = electron ? electron.ipcRenderer : null;
@@ -47,6 +48,28 @@ type State = {|
captureOptions: ?CaptureOptions,
|};
const prepareExporter = async (): Promise<{|
outputDir: string,
exporter: gdjsExporter,
gdjsRoot: string,
|}> => {
const { gdjsRoot } = await findGDJS();
console.info('GDJS found in ', gdjsRoot);
const localFileSystem = new LocalFileSystem({
downloadUrlsToLocalFiles: false,
});
const fileSystem = assignIn(new gd.AbstractFileSystemJS(), localFileSystem);
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
return {
outputDir,
exporter,
gdjsRoot,
};
};
export default class LocalPreviewLauncher extends React.Component<
PreviewLauncherProps,
State
@@ -171,168 +194,138 @@ export default class LocalPreviewLauncher extends React.Component<
);
};
_prepareExporter = (): Promise<{|
outputDir: string,
exporter: gdjsExporter,
gdjsRoot: string,
|}> => {
return findGDJS().then(({ gdjsRoot }) => {
console.info('GDJS found in ', gdjsRoot);
const localFileSystem = new LocalFileSystem({
downloadUrlsToLocalFiles: false,
});
const fileSystem = assignIn(
new gd.AbstractFileSystemJS(),
localFileSystem
);
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
return {
outputDir,
exporter,
gdjsRoot,
};
});
};
launchPreview = (previewOptions: PreviewOptions): Promise<any> => {
launchPreview = async (previewOptions: PreviewOptions): Promise<any> => {
const { project, layout, externalLayout } = previewOptions;
// Start the debugger server for previews. Even if not used,
// useful if the user opens the Debugger editor later, or want to
// hot reload.
return this.getPreviewDebuggerServer()
.startServer()
.catch(err => {
// Ignore any error when running the debugger server - the preview
// can still work without it.
console.error(
'Unable to start the Debugger Server for the preview:',
err
);
})
.then(() => this._prepareExporter())
.then(({ outputDir, exporter, gdjsRoot }) => {
timeFunction(
() => {
const previewExportOptions = new gd.PreviewExportOptions(
project,
outputDir
);
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
previewExportOptions.setLayoutName(layout.getName());
if (externalLayout) {
previewExportOptions.setExternalLayoutName(
externalLayout.getName()
);
}
try {
await this.getPreviewDebuggerServer().startServer();
} catch (err) {
console.error(
'Unable to start the Debugger Server for the preview:',
err
);
}
const previewDebuggerServerAddress = getDebuggerServerAddress();
if (previewDebuggerServerAddress) {
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
previewDebuggerServerAddress.address,
'' + previewDebuggerServerAddress.port
);
}
const { outputDir, exporter, gdjsRoot } = await prepareExporter();
const includeFileHashs = this.props.getIncludeFileHashs();
for (const includeFile in includeFileHashs) {
const hash = includeFileHashs[includeFile];
previewExportOptions.setIncludeFileHash(includeFile, hash);
}
var previewStartTime = performance.now();
// Give the preview the path to the "@electron/remote" module of the editor,
// as this is required by some features and we've not removed dependency
// on "@electron/remote" yet.
previewExportOptions.setElectronRemoteRequirePath(
path.join(
gdjsRoot,
'../preview_node_modules',
'@electron/remote',
'renderer/index.js'
)
);
const previewExportOptions = new gd.PreviewExportOptions(
project,
outputDir
);
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
previewExportOptions.setLayoutName(layout.getName());
previewExportOptions.setIsInGameEdition(previewOptions.isForInGameEdition);
if (externalLayout) {
previewExportOptions.setExternalLayoutName(externalLayout.getName());
}
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
const shouldHotReload =
previewOptions.hotReload && !!debuggerIds.length;
const previewDebuggerServerAddress = getDebuggerServerAddress();
if (previewDebuggerServerAddress) {
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
previewDebuggerServerAddress.address,
'' + previewDebuggerServerAddress.port
);
}
previewExportOptions.setProjectDataOnlyExport(
// Only export project data if asked and if a hot-reloading is being done.
shouldHotReload && previewOptions.projectDataOnlyExport
);
const includeFileHashs = this.props.getIncludeFileHashs();
for (const includeFile in includeFileHashs) {
const hash = includeFileHashs[includeFile];
previewExportOptions.setIncludeFileHash(includeFile, hash);
}
previewExportOptions.setFullLoadingScreen(
previewOptions.fullLoadingScreen
);
previewExportOptions.setGDevelopVersionWithHash(
getIDEVersionWithHash()
);
previewExportOptions.setCrashReportUploadLevel(
this.props.crashReportUploadLevel
);
previewExportOptions.setPreviewContext(this.props.previewContext);
previewExportOptions.setProjectTemplateSlug(
project.getTemplateSlug()
);
previewExportOptions.setSourceGameId(this.props.sourceGameId);
// Give the preview the path to the "@electron/remote" module of the editor,
// as this is required by some features and we've not removed dependency
// on "@electron/remote" yet.
previewExportOptions.setElectronRemoteRequirePath(
path.join(
gdjsRoot,
'../preview_node_modules',
'@electron/remote',
'renderer/index.js'
)
);
if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(
previewOptions.fallbackAuthor.id,
previewOptions.fallbackAuthor.username
);
}
if (previewOptions.authenticatedPlayer) {
previewExportOptions.setAuthenticatedPlayer(
previewOptions.authenticatedPlayer.playerId,
previewOptions.authenticatedPlayer.playerUsername,
previewOptions.authenticatedPlayer.playerToken
);
}
if (previewOptions.captureOptions) {
if (previewOptions.captureOptions.screenshots) {
previewOptions.captureOptions.screenshots.forEach(
screenshot => {
previewExportOptions.addScreenshotCapture(
screenshot.delayTimeInSeconds,
screenshot.signedUrl,
screenshot.publicUrl
);
}
);
}
}
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
const shouldHotReload = previewOptions.hotReload && !!debuggerIds.length;
exporter.exportProjectForPixiPreview(previewExportOptions);
previewExportOptions.delete();
exporter.delete();
previewExportOptions.setProjectDataOnlyExport(
// Only export project data if asked and if a hot-reloading is being done.
shouldHotReload && previewOptions.projectDataOnlyExport
);
if (shouldHotReload) {
debuggerIds.forEach(debuggerId => {
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
command: 'hotReload',
});
});
previewExportOptions.setFullLoadingScreen(previewOptions.fullLoadingScreen);
previewExportOptions.setGDevelopVersionWithHash(getIDEVersionWithHash());
previewExportOptions.setCrashReportUploadLevel(
this.props.crashReportUploadLevel
);
previewExportOptions.setPreviewContext(this.props.previewContext);
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
previewExportOptions.setSourceGameId(this.props.sourceGameId);
if (
this.state.hotReloadsCount % 16 === 0 &&
this._hotReloadSubscriptionChecker
) {
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
}
this.setState(state => ({
hotReloadsCount: state.hotReloadsCount + 1,
}));
} else {
this._openPreviewWindow(project, outputDir, previewOptions);
}
},
time => console.info(`Preview took ${time}ms`)
);
if (previewOptions.fallbackAuthor) {
previewExportOptions.setFallbackAuthor(
previewOptions.fallbackAuthor.id,
previewOptions.fallbackAuthor.username
);
}
if (previewOptions.authenticatedPlayer) {
previewExportOptions.setAuthenticatedPlayer(
previewOptions.authenticatedPlayer.playerId,
previewOptions.authenticatedPlayer.playerUsername,
previewOptions.authenticatedPlayer.playerToken
);
}
if (previewOptions.captureOptions) {
if (previewOptions.captureOptions.screenshots) {
previewOptions.captureOptions.screenshots.forEach(screenshot => {
previewExportOptions.addScreenshotCapture(
screenshot.delayTimeInSeconds,
screenshot.signedUrl,
screenshot.publicUrl
);
});
}
}
exporter.exportProjectForPixiPreview(previewExportOptions);
previewExportOptions.delete();
exporter.delete();
if (shouldHotReload) {
debuggerIds.forEach(debuggerId => {
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
command: 'hotReload',
});
});
if (
this.state.hotReloadsCount % 16 === 0 &&
this._hotReloadSubscriptionChecker
) {
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
}
this.setState(state => ({
hotReloadsCount: state.hotReloadsCount + 1,
}));
} else {
if (previewOptions.isForInGameEdition) {
attachToPreview({
previewIndexHtmlLocation: `file://${outputDir}/index.html`,
});
}
if (previewOptions.numberOfWindows >= 1) {
this._openPreviewWindow(project, outputDir, previewOptions);
}
}
const previewStopTime = performance.now();
console.info(`Preview took ${previewStopTime - previewStartTime}ms`);
};
getPreviewDebuggerServer() {

View File

@@ -15,6 +15,10 @@ export type LaunchPreviewOptions = {
fullLoadingScreen?: boolean,
forceDiagnosticReport?: boolean,
numberOfWindows?: number,
isForInGameEdition?: {|
forcedSceneName: string,
forcedExternalLayoutName: ?string,
|},
launchCaptureOptions?: LaunchCaptureOptions,
};
export type CaptureOptions = {|
@@ -40,6 +44,7 @@ export type PreviewOptions = {|
playerToken: string,
},
numberOfWindows: number,
isForInGameEdition: boolean,
getIsMenuBarHiddenInPreview: () => boolean,
getIsAlwaysOnTopInPreview: () => boolean,
captureOptions: CaptureOptions,
@@ -59,6 +64,13 @@ export type PreviewLauncherProps = {|
/** Each game connected to the debugger server is identified by a unique number. */
export type DebuggerId = number;
/** Each game connected to the debugger server can communicate its status. */
export type DebuggerStatus = {|
isPaused: boolean,
isInGameEdition: boolean,
sceneName: string | null,
|};
/** The callbacks for a debugger server used for previews. */
export type PreviewDebuggerServerCallbacks = {|
onErrorReceived: (err: Error) => void | Promise<void>,

View File

@@ -5,11 +5,15 @@ import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
const electron = optionalRequire('electron');
const ipcRenderer = electron ? electron.ipcRenderer : null;
type Props = {
onGDJSUpdated: () => Promise<void> | void,
};
/**
* Set up some watchers for GDJS and Extensions sources.
* Stop the watchers when the component is unmounted or `shouldWatch` prop is false.
*/
export const LocalGDJSDevelopmentWatcher = () => {
export const LocalGDJSDevelopmentWatcher = ({ onGDJSUpdated }: Props) => {
const preferences = React.useContext(PreferencesContext);
const shouldWatch = preferences.values.useGDJSDevelopmentWatcher;
@@ -35,5 +39,35 @@ export const LocalGDJSDevelopmentWatcher = () => {
[shouldWatch]
);
React.useEffect(
() => {
if (!shouldWatch) {
// Nothing to set up in the effect if watch is deactivated.
return;
}
if (!ipcRenderer) {
return;
}
ipcRenderer.removeAllListeners(
'local-gdjs-development-watcher-runtime-updated'
);
ipcRenderer.on(
'local-gdjs-development-watcher-runtime-updated',
(event, err) => {
onGDJSUpdated();
}
);
return () => {
ipcRenderer.removeAllListeners(
'local-gdjs-development-watcher-runtime-updated'
);
};
},
[shouldWatch, onGDJSUpdated]
);
return null;
};

View File

@@ -97,7 +97,7 @@ export const create = (authentication: Authentication) => {
)}
quickPublishOnlineWebExporter={localOnlineWebExporter}
renderGDJSDevelopmentWatcher={
isDev ? () => <LocalGDJSDevelopmentWatcher /> : null
isDev ? ({ onGDJSUpdated }) => <LocalGDJSDevelopmentWatcher onGDJSUpdated={onGDJSUpdated} /> : null
}
storageProviders={storageProviders}
resourceMover={LocalResourceMover}

View File

@@ -27,6 +27,9 @@ import {
} from '../ResourcesWatcher';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
const styles = {
container: {
@@ -69,6 +72,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
layout ? layout.getName() : null,
projectItemName
);
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
switchToSceneEdition({
sceneName: layout.getName(),
externalLayoutName: projectItemName,
});
}
}
this.resourceExternallyChangedCallbackId = registerOnResourceExternallyChangedCallback(
this.onResourceExternallyChanged.bind(this)
@@ -88,6 +98,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
layout ? layout.getName() : null,
projectItemName
);
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
switchToSceneEdition({
sceneName: layout.getName(),
externalLayoutName: projectItemName,
});
}
}
}

View File

@@ -12,6 +12,9 @@ import {
} from './BaseEditor';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
export class SceneEditorContainer extends React.Component<RenderEditorContainerProps> {
editor: ?SceneEditor;
@@ -32,6 +35,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
if (this.props.isActive) {
const { projectItemName } = this.props;
this.props.setPreviewedLayout(projectItemName);
if (gameEditorMode === 'embedded-game' && projectItemName) {
switchToSceneEdition({ sceneName: projectItemName });
}
}
}
@@ -39,6 +46,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
if (!prevProps.isActive && this.props.isActive) {
const { projectItemName } = this.props;
this.props.setPreviewedLayout(projectItemName);
if (gameEditorMode === 'embedded-game' && projectItemName) {
switchToSceneEdition({ sceneName: projectItemName });
}
}
}

View File

@@ -48,6 +48,8 @@ export type EditorTab = {|
extraEditorProps: ?EditorContainerExtraProps,
/** If set to false, the tab can't be closed. */
closable: boolean,
/** If set to true, `pointer-events: none` is applied to the tab content. */
removePointerEvents: boolean,
|};
export type EditorTabsState = {|
@@ -91,6 +93,7 @@ export type EditorOpeningOptions = {|
extraEditorProps?: EditorContainerExtraProps,
dontFocusTab?: boolean,
closable?: boolean,
removePointerEvents?: boolean,
|};
export const getEditorTabMetadata = (
@@ -138,6 +141,7 @@ export const openEditorTab = (
key,
extraEditorProps,
dontFocusTab,
removePointerEvents,
closable,
}: EditorOpeningOptions
): EditorTabsState => {
@@ -163,6 +167,7 @@ export const openEditorTab = (
extraEditorProps,
editorRef: null,
closable: typeof closable === 'undefined' ? true : !!closable,
removePointerEvents: !!removePointerEvents,
};
return {

View File

@@ -4,6 +4,7 @@ import {
type PreviewDebuggerServer,
type DebuggerId,
type HotReloaderLog,
type DebuggerStatus,
} from '../ExportAndShare/PreviewLauncher.flow';
/** Represents what should be run when a preview is launched */
@@ -22,26 +23,31 @@ export type PreviewState = {|
|};
type PreviewDebuggerServerWatcherResults = {|
previewDebuggerIds: Array<DebuggerId>,
hasNonEditionPreviewsRunning: boolean,
hotReloadLogs: Array<HotReloaderLog>,
clearHotReloadLogs: () => void,
hardReloadAllPreviews: () => void,
|};
/**
* Return the ids of the debuggers being run, watching for changes (new
* Return the status of the debuggers being run, watching for changes (new
* debugger launched or existing one closed).
*/
export const usePreviewDebuggerServerWatcher = (
previewDebuggerServer: ?PreviewDebuggerServer
): PreviewDebuggerServerWatcherResults => {
const [debuggerIds, setDebuggerIds] = React.useState<Array<DebuggerId>>([]);
const [debuggerStatus, setDebuggerStatus] = React.useState<{
[DebuggerId]: DebuggerStatus,
}>({});
const [hotReloadLogs, setHotReloadLogs] = React.useState<
Array<HotReloaderLog>
>([]);
React.useEffect(
() => {
if (!previewDebuggerServer) {
setDebuggerIds([]);
setDebuggerStatus({});
return;
}
@@ -50,10 +56,24 @@ export const usePreviewDebuggerServerWatcher = (
// Nothing to do.
},
onConnectionClosed: ({ id, debuggerIds }) => {
setDebuggerIds([...debuggerIds]);
// Remove the debugger status.
setDebuggerStatus(debuggerStatus => {
const {
[id]: closedDebuggerStatus,
...otherDebuggerStatus
} = debuggerStatus;
console.info(
`Connection closed with preview #${id}. Last status was:`,
closedDebuggerStatus
);
return otherDebuggerStatus;
});
},
onConnectionOpened: ({ id, debuggerIds }) => {
setDebuggerIds([...debuggerIds]);
// Ask the new debugger client for its status (but don't assume anything
// at this stage).
previewDebuggerServer.sendMessage(id, { command: 'getStatus' });
},
onConnectionErrored: ({ id }) => {
// Nothing to do (onConnectionClosed is called if necessary).
@@ -64,6 +84,15 @@ export const usePreviewDebuggerServerWatcher = (
onHandleParsedMessage: ({ id, parsedMessage }) => {
if (parsedMessage.command === 'hotReloader.logs') {
setHotReloadLogs(parsedMessage.payload);
} else if (parsedMessage.command === 'status') {
setDebuggerStatus(debuggerStatus => ({
...debuggerStatus,
[id]: {
isPaused: !!parsedMessage.payload.isPaused,
isInGameEdition: !!parsedMessage.payload.isInGameEdition,
sceneName: parsedMessage.payload.sceneName,
},
}));
}
},
});
@@ -77,5 +106,28 @@ export const usePreviewDebuggerServerWatcher = (
setHotReloadLogs,
]);
return { previewDebuggerIds: debuggerIds, hotReloadLogs, clearHotReloadLogs };
const hardReloadAllPreviews = React.useCallback(
() => {
if (!previewDebuggerServer) return;
console.info('Hard reloading all previews...');
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
previewDebuggerServer.sendMessage(debuggerId, {
command: 'hardReload',
});
});
},
[previewDebuggerServer]
);
const hasNonEditionPreviewsRunning = Object.keys(debuggerStatus).some(
key => !debuggerStatus[+key].isInGameEdition
);
return {
hasNonEditionPreviewsRunning,
hotReloadLogs,
clearHotReloadLogs,
hardReloadAllPreviews,
};
};

View File

@@ -19,7 +19,12 @@ type Props = {|
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const styles = {
container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' },
container: {
display: 'flex',
flexShrink: 0,
alignItems: 'flex-end',
position: 'relative',
},
leftSideArea: { alignSelf: 'stretch', flexShrink: 0 },
rightSideArea: { alignSelf: 'stretch', flex: 1 },
menuIcon: { marginLeft: 4, marginRight: 4 },

View File

@@ -87,21 +87,21 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
click: async () => {
await onPreviewWithoutHotReload({ numberOfWindows: 2 });
},
enabled: isPreviewEnabled && !hasPreviewsRunning,
enabled: isPreviewEnabled,
},
{
label: i18n._(t`3 previews in 3 windows`),
click: async () => {
onPreviewWithoutHotReload({ numberOfWindows: 3 });
},
enabled: isPreviewEnabled && !hasPreviewsRunning,
enabled: isPreviewEnabled,
},
{
label: i18n._(t`4 previews in 4 windows`),
click: async () => {
onPreviewWithoutHotReload({ numberOfWindows: 4 });
},
enabled: isPreviewEnabled && !hasPreviewsRunning,
enabled: isPreviewEnabled,
},
],
},
@@ -185,7 +185,9 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
<LineStackLayout noMargin>
<FlatButtonWithSplitMenu
primary
onClick={onHotReloadPreview}
onClick={
hasPreviewsRunning ? onHotReloadPreview : onPreviewWithoutHotReload
}
disabled={!isPreviewEnabled}
icon={hasPreviewsRunning ? <UpdateIcon /> : <PreviewIcon />}
label={

View File

@@ -200,6 +200,7 @@ import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import useGamesList from '../GameDashboard/UseGamesList';
import useCapturesManager from './UseCapturesManager';
import useHomepageWitchForRouting from './UseHomepageWitchForRouting';
import { EmbeddedGameFrame } from '../EmbeddedGame/EmbeddedGameFrame';
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
@@ -305,7 +306,9 @@ export type Props = {|
resourceExternalEditors: Array<ResourceExternalEditor>,
requestUpdate?: () => void,
renderShareDialog: ShareDialogWithoutExportsProps => React.Node,
renderGDJSDevelopmentWatcher?: ?() => React.Node,
renderGDJSDevelopmentWatcher?: ?({|
onGDJSUpdated: () => Promise<void> | void,
|}) => React.Node,
extensionsLoader?: JsExtensionsLoader,
initialFileMetadataToOpen: ?FileMetadata,
initialExampleSlugToOpen: ?string,
@@ -409,11 +412,11 @@ const MainFrame = (props: Props) => {
_previewLauncher.current &&
_previewLauncher.current.getPreviewDebuggerServer();
const {
previewDebuggerIds,
hasNonEditionPreviewsRunning,
hotReloadLogs,
clearHotReloadLogs,
hardReloadAllPreviews,
} = usePreviewDebuggerServerWatcher(previewDebuggerServer);
const hasPreviewsRunning = !!previewDebuggerIds.length;
const {
ensureInteractionHappened,
renderOpenConfirmDialog,
@@ -620,6 +623,10 @@ const MainFrame = (props: Props) => {
<ExtensionIcon />
) : null;
// Scene editors can have an embedded game, so they redefine manually
// which components can have clicks/touches.
const removePointerEvents = kind === 'layout';
const closable = kind !== 'start page';
const extraEditorProps =
kind === 'start page'
@@ -637,6 +644,7 @@ const MainFrame = (props: Props) => {
)
: null,
closable,
removePointerEvents,
label,
projectItemName: name,
tabOptions,
@@ -1607,6 +1615,7 @@ const MainFrame = (props: Props) => {
fullLoadingScreen,
forceDiagnosticReport,
launchCaptureOptions,
isForInGameEdition,
}: LaunchPreviewOptions) => {
if (!currentProject) return;
if (currentProject.getLayoutsCount() === 0) return;
@@ -1617,10 +1626,14 @@ const MainFrame = (props: Props) => {
setPreviewLoading(true);
notifyPreviewOrExportWillStart(state.editorTabs);
const layoutName = previewState.isPreviewOverriden
const layoutName = isForInGameEdition
? isForInGameEdition.forcedSceneName
: previewState.isPreviewOverriden
? previewState.overridenPreviewLayoutName
: previewState.previewLayoutName;
const externalLayoutName = previewState.isPreviewOverriden
const externalLayoutName = isForInGameEdition
? isForInGameEdition.forcedExternalLayoutName
: previewState.isPreviewOverriden
? previewState.overridenPreviewExternalLayoutName
: previewState.previewExternalLayoutName;
@@ -1662,6 +1675,7 @@ const MainFrame = (props: Props) => {
const startTime = Date.now();
await previewLauncher.launchPreview({
project: currentProject,
// TODO: replace by scene name and external layout name
layout,
externalLayout,
networkPreview: !!networkPreview,
@@ -1672,23 +1686,25 @@ const MainFrame = (props: Props) => {
authenticatedPlayer,
getIsMenuBarHiddenInPreview: preferences.getIsMenuBarHiddenInPreview,
getIsAlwaysOnTopInPreview: preferences.getIsAlwaysOnTopInPreview,
numberOfWindows: numberOfWindows || 1,
numberOfWindows: numberOfWindows === undefined ? 1 : numberOfWindows,
isForInGameEdition: !!isForInGameEdition,
captureOptions,
onCaptureFinished,
});
setPreviewLoading(false);
sendPreviewStarted({
quickCustomizationGameId:
quickCustomizationDialogOpenedFromGameId || null,
networkPreview: !!networkPreview,
hotReload: !!hotReload,
projectDataOnlyExport: !!projectDataOnlyExport,
fullLoadingScreen: !!fullLoadingScreen,
numberOfWindows: numberOfWindows || 1,
forceDiagnosticReport: !!forceDiagnosticReport,
previewLaunchDuration: Date.now() - startTime,
});
if (!isForInGameEdition)
sendPreviewStarted({
quickCustomizationGameId:
quickCustomizationDialogOpenedFromGameId || null,
networkPreview: !!networkPreview,
hotReload: !!hotReload,
projectDataOnlyExport: !!projectDataOnlyExport,
fullLoadingScreen: !!fullLoadingScreen,
numberOfWindows: numberOfWindows || 1,
forceDiagnosticReport: !!forceDiagnosticReport,
previewLaunchDuration: Date.now() - startTime,
});
if (inAppTutorialOrchestratorRef.current) {
inAppTutorialOrchestratorRef.current.onPreviewLaunch();
@@ -1772,6 +1788,47 @@ const MainFrame = (props: Props) => {
[launchPreview]
);
const onLaunchPreviewForInGameEdition = React.useCallback(
({
sceneName,
externalLayoutName,
}: {|
sceneName: string,
externalLayoutName: ?string,
|}) => {
launchPreview({
networkPreview: false,
hotReload: false,
forceDiagnosticReport: false,
isForInGameEdition: {
forcedSceneName: sceneName,
forcedExternalLayoutName: externalLayoutName,
},
numberOfWindows: 0,
});
},
[launchPreview]
);
const relaunchAndThenHardReloadAllPreviews = React.useCallback(
async () => {
// Build a new preview (so that any changes in runtime files are picked up)
// and then ask all previews to "hard reload" themselves (i.e: refresh their page).
await launchPreview({
networkPreview: false,
hotReload: false,
forceDiagnosticReport: false,
numberOfWindows: 0,
});
hardReloadAllPreviews();
},
[
hardReloadAllPreviews,
launchPreview,
]
);
const launchQuickCustomizationPreview = React.useCallback(
() =>
launchPreview({
@@ -1790,7 +1847,7 @@ const MainFrame = (props: Props) => {
const hotReloadPreviewButtonProps: HotReloadPreviewButtonProps = React.useMemo(
() => ({
hasPreviewsRunning,
hasPreviewsRunning: hasNonEditionPreviewsRunning,
launchProjectWithLoadingScreenPreview: () =>
launchPreview({ fullLoadingScreen: true }),
launchProjectDataOnlyPreview: () =>
@@ -1798,7 +1855,7 @@ const MainFrame = (props: Props) => {
launchProjectCodeAndDataPreview: () =>
launchPreview({ hotReload: true, projectDataOnlyExport: false }),
}),
[hasPreviewsRunning, launchPreview]
[hasNonEditionPreviewsRunning, launchPreview]
);
const getEditorsTabStateWithScene = React.useCallback(
@@ -3475,7 +3532,7 @@ const MainFrame = (props: Props) => {
previewEnabled:
!!state.currentProject && state.currentProject.getLayoutsCount() > 0,
onOpenProjectManager: toggleProjectManager,
hasPreviewsRunning,
hasPreviewsRunning: hasNonEditionPreviewsRunning,
allowNetworkPreview:
!!_previewLauncher.current &&
_previewLauncher.current.canDoNetworkPreview(),
@@ -3570,6 +3627,30 @@ const MainFrame = (props: Props) => {
'main-frame' /* The root styling, done in CSS to read some CSS variables. */
}
>
{!!renderPreviewLauncher &&
renderPreviewLauncher(
{
crashReportUploadLevel:
preferences.values.previewCrashReportUploadLevel ||
'exclude-javascript-code-events',
previewContext: quickCustomizationDialogOpenedFromGameId
? 'preview-quick-customization'
: 'preview',
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
getIncludeFileHashs:
eventsFunctionsExtensionsContext.getIncludeFileHashs,
onExport: () => openShareDialog('publish'),
onCaptureFinished,
},
(previewLauncher: ?PreviewLauncherInterface) => {
_previewLauncher.current = previewLauncher;
}
)}
<EmbeddedGameFrame
key={currentProject ? currentProject.ptr : 0}
previewDebuggerServer={previewDebuggerServer || null}
onLaunchPreviewForInGameEdition={onLaunchPreviewForInGameEdition}
/>
{!!renderMainMenu &&
renderMainMenu(
{ ...buildMainMenuProps, isApplicationTopLevelMenu: true },
@@ -3677,7 +3758,7 @@ const MainFrame = (props: Props) => {
!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId
}
onOpenDebugger={launchDebuggerAndPreview}
hasPreviewsRunning={hasPreviewsRunning}
hasPreviewsRunning={hasNonEditionPreviewsRunning}
onPreviewWithoutHotReload={launchNewPreview}
onNetworkPreview={launchNetworkPreview}
onHotReloadPreview={launchHotReloadPreview}
@@ -3706,7 +3787,11 @@ const MainFrame = (props: Props) => {
const errorBoundaryProps = getEditorErrorBoundaryProps(editorTab.key);
return (
<TabContentContainer key={editorTab.key} active={isCurrentTab}>
<TabContentContainer
key={editorTab.key}
active={isCurrentTab}
removePointerEvents={editorTab.removePointerEvents}
>
<CommandsContextScopedProvider active={isCurrentTab}>
<ErrorBoundary
componentTitle={errorBoundaryProps.componentTitle}
@@ -3837,7 +3922,11 @@ const MainFrame = (props: Props) => {
<LoaderModal
show={showLoader}
progress={fileMetadataOpeningProgress}
message={loaderModalOpeningMessage || fileMetadataOpeningMessage}
message={
loaderModalOpeningMessage ||
fileMetadataOpeningMessage ||
(previewLoading ? t`Loading preview...` : null)
}
/>
<Snackbar
open={state.snackMessageOpen}
@@ -3860,25 +3949,6 @@ const MainFrame = (props: Props) => {
initialTab: shareDialogInitialTab,
gamesList,
})}
{!!renderPreviewLauncher &&
renderPreviewLauncher(
{
crashReportUploadLevel:
preferences.values.previewCrashReportUploadLevel ||
'exclude-javascript-code-events',
previewContext: quickCustomizationDialogOpenedFromGameId
? 'preview-quick-customization'
: 'preview',
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
getIncludeFileHashs:
eventsFunctionsExtensionsContext.getIncludeFileHashs,
onExport: () => openShareDialog('publish'),
onCaptureFinished,
},
(previewLauncher: ?PreviewLauncherInterface) => {
_previewLauncher.current = previewLauncher;
}
)}
{chooseResourceOptions && onResourceChosen && !!currentProject && (
<NewResourceDialog
project={currentProject}
@@ -4011,7 +4081,9 @@ const MainFrame = (props: Props) => {
)}
{state.gdjsDevelopmentWatcherEnabled &&
renderGDJSDevelopmentWatcher &&
renderGDJSDevelopmentWatcher()}
renderGDJSDevelopmentWatcher({
onGDJSUpdated: relaunchAndThenHardReloadAllPreviews,
})}
{!!hotReloadLogs.length && (
<HotReloadLogsDialog
logs={hotReloadLogs}

View File

@@ -307,7 +307,7 @@ const CollisionMasksEditor = ({
if (!animations.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
const editors: { [string]: Editor | null } = {
preview: {
type: 'primary',
noTitleBar: true,

View File

@@ -205,7 +205,7 @@ const PointsEditor = ({
if (!animations.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
const editors: { [string]: Editor | null } = {
preview: {
type: 'primary',
noTitleBar: true,

View File

@@ -21,6 +21,7 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even
import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
export type SceneEditorsDisplayProps = {|
gameEditorMode: 'embedded-game' | 'instances-editor',
project: gdProject,
layout: gdLayout | null,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,

View File

@@ -82,6 +82,7 @@ const MosaicEditorsDisplay = React.forwardRef<
SceneEditorsDisplayInterface
>((props, ref) => {
const {
gameEditorMode,
project,
resourceManagementProps,
layout,
@@ -323,46 +324,49 @@ const MosaicEditorsDisplay = React.forwardRef<
/>
),
},
'instances-editor': {
type: 'primary',
noTitleBar: true,
noSoftKeyboardAvoidance: true,
renderEditor: () => (
<FullSizeInstancesEditorWithScrollbars
project={project}
layout={layout}
eventsBasedObject={eventsBasedObject}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
layersContainer={layersContainer}
selectedLayer={selectedLayer}
initialInstances={initialInstances}
instancesEditorSettings={props.instancesEditorSettings}
onInstancesEditorSettingsMutated={
props.onInstancesEditorSettingsMutated
}
instancesSelection={props.instancesSelection}
onInstancesAdded={props.onInstancesAdded}
onInstancesSelected={props.onInstancesSelected}
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
props.instancesEditorShortcutsCallbacks
}
wrappedEditorRef={editor => {
editorRef.current = editor;
}}
pauseRendering={!props.isActive}
tileMapTileSelection={props.tileMapTileSelection}
onSelectTileMapTile={props.onSelectTileMapTile}
/>
),
},
'instances-editor':
gameEditorMode === 'embedded-game'
? null
: {
type: 'primary',
noTitleBar: true,
noSoftKeyboardAvoidance: true,
renderEditor: () => (
<FullSizeInstancesEditorWithScrollbars
project={project}
layout={layout}
eventsBasedObject={eventsBasedObject}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
layersContainer={layersContainer}
selectedLayer={selectedLayer}
initialInstances={initialInstances}
instancesEditorSettings={props.instancesEditorSettings}
onInstancesEditorSettingsMutated={
props.onInstancesEditorSettingsMutated
}
instancesSelection={props.instancesSelection}
onInstancesAdded={props.onInstancesAdded}
onInstancesSelected={props.onInstancesSelected}
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
props.instancesEditorShortcutsCallbacks
}
wrappedEditorRef={editor => {
editorRef.current = editor;
}}
pauseRendering={!props.isActive}
tileMapTileSelection={props.tileMapTileSelection}
onSelectTileMapTile={props.onSelectTileMapTile}
/>
),
},
'objects-list': {
type: 'secondary',
title: t`Objects`,
@@ -448,6 +452,7 @@ const MosaicEditorsDisplay = React.forwardRef<
),
},
};
return (
<EditorMosaic
editors={editors}

View File

@@ -141,6 +141,7 @@ type Props = {|
|};
type State = {|
gameEditorMode: 'embedded-game' | 'instances-editor',
setupGridOpen: boolean,
scenePropertiesDialogOpen: boolean,
layersListOpen: boolean,
@@ -196,6 +197,7 @@ export default class SceneEditor extends React.Component<Props, State> {
this.instancesSelection = new InstancesSelection();
this.state = {
gameEditorMode: 'embedded-game',
setupGridOpen: false,
scenePropertiesDialogOpen: false,
layersListOpen: false,
@@ -1955,6 +1957,7 @@ export default class SceneEditor extends React.Component<Props, State> {
/>
<EditorsDisplay
ref={ref => (this.editorDisplay = ref)}
gameEditorMode={this.state.gameEditorMode}
project={project}
layout={layout}
eventsFunctionsExtension={eventsFunctionsExtension}

View File

@@ -45,6 +45,7 @@ const styles = {
type TabContentContainerProps = {|
active: boolean,
removePointerEvents?: boolean,
children: React.Node,
|};
@@ -69,6 +70,7 @@ export class TabContentContainer extends React.Component<TabContentContainerProp
style={{
...styles.tabContentContainer,
...(active ? undefined : { display: 'none' }),
pointerEvents: this.props.removePointerEvents ? 'none' : undefined,
}}
>
{children}

View File

@@ -252,7 +252,7 @@ export type EditorMosaicInterface = {|
type Props = {|
initialNodes: EditorMosaicNode,
editors: {
[string]: Editor,
[string]: Editor | null,
},
limitToOneSecondaryEditor?: boolean,
onOpenedEditorsChanged?: () => void,
@@ -300,7 +300,8 @@ const EditorMosaic = React.forwardRef<Props, EditorMosaicInterface>(
if (limitToOneSecondaryEditor && editor.type === 'secondary') {
// Replace the existing secondary editor, if any.
const secondaryEditorName = openedEditorNames.find(
editorName => editors[editorName].type === 'secondary'
editorName =>
editors[editorName] && editors[editorName].type === 'secondary'
);
if (secondaryEditorName) {
setMosaicNode(
@@ -408,14 +409,18 @@ const EditorMosaic = React.forwardRef<Props, EditorMosaicInterface>(
// Move the entire mosaic up when the soft keyboard is open:
'avoid-soft-keyboard': true,
})}
style={{ position: 'relative', width: '100%', height: '100%' }}
renderTile={(editorName: string, path: string) => {
const editor = editors[editorName];
if (!editor) {
if (editor === undefined) {
console.error(
'Trying to render un unknown editor: ' + editorName
);
return null;
}
if (editor === null) {
return null;
}
if (editor.noTitleBar) {
return editor.renderEditor();

View File

@@ -7,10 +7,10 @@
}
.mosaic-gd-theme.mosaic .mosaic-root {
left: 0;
/* left: 0;
right: 0;
top: 0;
bottom: 0;
bottom: 0; */
}
.mosaic-gd-theme .mosaic-window {

View File

@@ -28,14 +28,15 @@ type Props = {|
progress?: ?number,
|};
const transitionDuration = { enter: 0, exit: 150 };
const transitionDuration = { enter: 400 };
const LoaderModal = ({ progress, message, show }: Props) => {
const isInfinite = progress === null || progress === undefined;
if (!show) return null;
return (
<I18n>
{({ i18n }) => (
<Dialog open={show} transitionDuration={transitionDuration}>
<Dialog open transitionDuration={transitionDuration}>
<DialogContent style={styles.dialogContent}>
<div
style={{

View File

@@ -1,6 +1,11 @@
/* Mosaic layout */
.mosaic-gd-theme.mosaic {
background-color: var(--mosaic-layout-background-color) !important;
/* background-color: var(--mosaic-layout-background-color) !important; */
background-color: transparent !important;
}
.mosaic-drop-target .drop-target-container {
pointer-events: all;
}
/* Mosaic window and tile */
@@ -27,6 +32,14 @@
background: var(--mosaic-toolbar-background-color) !important;
}
.mosaic-gd-theme .mosaic-window {
pointer-events: all;
}
.mosaic-gd-theme .mosaic-split {
pointer-events: all;
}
.mosaic-gd-theme .mosaic-split.-column {
background-color: var(--mosaic-layout-background-color) !important;
}

View File

@@ -17,6 +17,7 @@ const styles = {
overflowY: 'hidden',
paddingLeft: 8,
paddingRight: 8,
position: 'relative',
},
};

View File

@@ -40,7 +40,7 @@ module.exports = {
log.info(`Debugger connection #${id} opened.`);
newWebSocket.on('message', message => {
log.info(`Debugger connection #${id} received message.`);
// log.info(`Debugger connection #${id} received message.`);
options.onMessage({ id, message });
});

View File

@@ -7,6 +7,8 @@ const process = require('process');
const path = require('path');
const log = require('electron-log');
let onRuntimeUpdatedCallback = () => {};
/**
* Returns the folder corresponding to newIDE/app in **development**.
* @returns {string}
@@ -69,7 +71,11 @@ const onWatchEvent = debounce(
log.info(
`GDJS/extensions watchers found a "${eventName}" in ${resolvedFilename}, updating GDJS Runtime...`
);
importGDJSRuntime().catch(() => {});
importGDJSRuntime()
.then(() => {
onRuntimeUpdatedCallback();
})
.catch(() => {});
},
100 /* Avoid running the script too much in case multiple changes are fired at the same time. */
);
@@ -101,7 +107,7 @@ const setupLocalGDJSDevelopmentWatcher = () => {
path.join(findDevelopmentNewIdeAppPath(), watchPath)
);
// Reload extensions when the component is first mounted
// Reload extensions when the watcher is first set.
importGDJSRuntime().catch(() => {});
watcher = chokidar
@@ -127,7 +133,12 @@ const closeLocalGDJSDevelopmentWatcher = () => {
watcher = null;
};
const onLocalGDJSDevelopmentWatcherRuntimeUpdated = cb => {
onRuntimeUpdatedCallback = cb;
};
module.exports = {
setupLocalGDJSDevelopmentWatcher,
closeLocalGDJSDevelopmentWatcher,
onLocalGDJSDevelopmentWatcherRuntimeUpdated,
};

View File

@@ -30,6 +30,7 @@ const { openPreviewWindow, closePreviewWindow } = require('./PreviewWindow');
const {
setupLocalGDJSDevelopmentWatcher,
closeLocalGDJSDevelopmentWatcher,
onLocalGDJSDevelopmentWatcherRuntimeUpdated,
} = require('./LocalGDJSDevelopmentWatcher');
const { setupWatcher, disableWatcher } = require('./LocalFilesystemWatcher');
@@ -326,6 +327,11 @@ app.on('ready', function() {
closeLocalGDJSDevelopmentWatcher();
});
onLocalGDJSDevelopmentWatcherRuntimeUpdated(() => {
log.info('Notifying the editor that the GDJS runtime has been updated.');
mainWindow.webContents.send('local-gdjs-development-watcher-runtime-updated', null);
});
// DebuggerServer events:
ipcMain.on('debugger-start-server', (event, options) => {
log.info('Received event to start debugger server with options=', options);