mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
12 Commits
cursor/sco
...
feature/em
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa2371274d | ||
![]() |
0aea8dfa0f | ||
![]() |
81ca18098d | ||
![]() |
b6e44a022f | ||
![]() |
1a8eee2477 | ||
![]() |
d0ef92da03 | ||
![]() |
9c98cb3b3b | ||
![]() |
3681542056 | ||
![]() |
7c0bf135d7 | ||
![]() |
9a31dd046c | ||
![]() |
74401a1f9c | ||
![]() |
cedc6ea3e9 |
@@ -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 =
|
||||
|
@@ -320,7 +320,7 @@ namespace gdjs {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
|
@@ -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")
|
||||
|
@@ -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;
|
||||
|
@@ -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',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
|
1
GDevelop.js/types.d.ts
vendored
1
GDevelop.js/types.d.ts
vendored
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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];
|
||||
};
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
{
|
||||
|
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal file
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal 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>
|
||||
);
|
||||
};
|
@@ -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(
|
||||
|
@@ -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() {
|
||||
|
@@ -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>,
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
@@ -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 },
|
||||
|
@@ -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={
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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();
|
||||
|
@@ -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 {
|
||||
|
@@ -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={{
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ const styles = {
|
||||
overflowY: 'hidden',
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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 });
|
||||
});
|
||||
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user