Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental, WIP] In-game scene editor #7261

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion Core/GDCore/Project/PropertyDescriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion Extensions/TextObject/textruntimeobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ namespace gdjs {
return this._renderer.getRendererObject();
}

update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
this._renderer.ensureUpToDate();
}

Expand Down
17 changes: 15 additions & 2 deletions GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

auto &initialRuntimeGameStatus =
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
initialRuntimeGameStatus.AddChild("sceneName")
.SetStringValue(options.layoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
}
if (!options.externalLayoutName.empty()) {
runtimeGameOptions.AddChild("injectExternalLayout")
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
.SetValue(options.externalLayoutName);

if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
.SetBoolValue(true);
}
}

runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("nativeMobileApp")
Expand Down
10 changes: 10 additions & 0 deletions GDJS/GDJS/IDE/ExporterHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ struct PreviewExportOptions {
projectDataOnlyExport(false),
fullLoadingScreen(false),
isDevelopmentEnvironment(false),
isInGameEdition(false),
nonRuntimeScriptsCacheBurst(0),
fallbackAuthorId(""),
fallbackAuthorUsername(""),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -291,6 +300,7 @@ struct PreviewExportOptions {
bool projectDataOnlyExport;
bool fullLoadingScreen;
bool isDevelopmentEnvironment;
bool isInGameEdition;
unsigned int nonRuntimeScriptsCacheBurst;
gd::String electronRemoteRequirePath;
gd::String gdevelopResourceToken;
Expand Down
114 changes: 96 additions & 18 deletions GDJS/Runtime/debugger-client/abstract-debugger-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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.'
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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',
})
);
}
Expand Down
96 changes: 47 additions & 49 deletions GDJS/Runtime/debugger-client/hot-reloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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(
Expand Down
Loading
Loading