Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/Editor/Panels/EditorToolbarPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "Editor/EditorPersistence.h"
#include "Editor/ExportBuilder.h"
#include "Editor/PlayerLauncher.h"
#include "Engine/EditorExceptionGuard.h"
#include "Game/Game.h"
#include "Game/GameConfig.h"
#include "imgui.h"
Expand Down Expand Up @@ -106,10 +107,14 @@ void DrawToolbar(Game* game, bool& showProjectSelector, bool& openSaveLayoutModa
// Disabled while already running so the button reads as the current state.
ImGui::BeginDisabled(!engineOptions.isPaused);
if (ImGui::Button("Play")) {
// ReloadScene runs the scene's Lua, which can throw. Guard it so a broken scene logs +
// stays paused instead of crashing the editor (toolbar handlers run during ImGui render,
// outside FrameLoop's per-frame Update guard). See EditorExceptionGuard.h.
bool started = true;
if (!game->IsSceneRunning() && hasScene) {
game->ReloadScene();
started = RunEditorGuarded(gameConfig, "ReloadScene", [&] { game->ReloadScene(); });
}
engineOptions.isPaused = false;
engineOptions.isPaused = !started;
}
ImGui::EndDisabled();
ImGui::SameLine();
Expand All @@ -126,7 +131,7 @@ void DrawToolbar(Game* game, bool& showProjectSelector, bool& openSaveLayoutModa
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
game->StopScene();
RunEditorGuarded(gameConfig, "StopScene", [&] { game->StopScene(); });
engineOptions.isPaused = true; // back to the default ready-but-paused state
}

Expand Down
45 changes: 45 additions & 0 deletions src/Engine/EditorExceptionGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include <exception>
#include <string>

#include "Game/GameConfig.h"
#include "General/Logger.h"

namespace octarine::editor {

// Run a piece of game-driving work (`fn`) that may execute project/Lua code and could throw.
//
// In an editor session a project bug must not take down the whole editor process. When `fn` throws
// there, the exception is caught, logged (so it surfaces in the editor console), and the running
// game is paused — leaving the editor alive so the author can read the error, fix the script, and
// hit Play again. Pausing on catch also stops a throwing per-frame tick from re-raising the same
// exception every frame.
//
// Outside an editor session (the standalone player, headless bench/bake tooling) the exception is
// left to propagate exactly as before: the player runs in its own process, so a crash there never
// reaches the editor, and we don't want to silently swallow failures in shipped/CI paths.
//
// `context` is a short human label for the failing phase (e.g. "Update", "Input", "ReloadScene")
// included in the log line. Returns true when `fn` completed without throwing, false when it was
// caught.
template <typename Fn>
bool RunEditorGuarded(GameConfig& config, const char* context, Fn&& fn) {
if (!config.IsEditorMode()) {
fn();
return true;
}

try {
fn();
return true;
} catch (const std::exception& ex) {
Logger::Error(std::string("[editor] Game exception in ") + context + ": " + ex.what() + " — execution paused.");
} catch (...) {
Logger::Error(std::string("[editor] Unknown game exception in ") + context + " — execution paused.");
}
config.GetEngineOptions().isPaused = true;
return false;
}

} // namespace octarine::editor
24 changes: 19 additions & 5 deletions src/Engine/FrameLoop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "AssetManager/AssetManager.h"
#include "Components/ViewportInfo.h"
#include "ECS/Registry.h"
#include "Engine/EditorExceptionGuard.h"
#include "Engine/EngineContext.h"
#include "Engine/EngineRuntime.h"
#include "EventBus/EventBus.h"
Expand Down Expand Up @@ -83,6 +84,11 @@ void FrameLoop::ProcessInput() {
PROFILE_NAMED_SCOPE("Game::ProcessInput");
SDL_Event event;

// Mouse/key event emission fans out to game code (Lua input handlers, UIButtonSystem callbacks),
// so it is guarded the same way as the per-frame Update — a throwing handler pauses rather than
// crashes the editor. See FrameLoop::Update / EditorExceptionGuard.h.
auto& gameConfig = registry_->Get<GameConfig>();

while (SDL_PollEvent(&event)) {
#ifdef OCTARINE_WITH_IMGUI
ImGui_ImplSDL3_ProcessEvent(&event);
Expand All @@ -102,17 +108,20 @@ void FrameLoop::ProcessInput() {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
KeyInputEvent keyInputEvent = GetKeyInputEvent(&event.key);
event_bus_->EmitEvent<KeyInputEvent>(keyInputEvent);
octarine::editor::RunEditorGuarded(gameConfig, "Input",
[&] { event_bus_->EmitEvent<KeyInputEvent>(keyInputEvent); });
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP: {
SDL_MouseButtonEvent mouseButtonEvent = event.button;
event_bus_->EmitEvent<MouseInputEvent>(mouseButtonEvent);
octarine::editor::RunEditorGuarded(gameConfig, "Input",
[&] { event_bus_->EmitEvent<MouseInputEvent>(mouseButtonEvent); });
break;
}
case SDL_EVENT_MOUSE_WHEEL: {
event_bus_->EmitEvent<MouseWheelEvent>(event.wheel.x, event.wheel.y);
octarine::editor::RunEditorGuarded(
gameConfig, "Input", [&] { event_bus_->EmitEvent<MouseWheelEvent>(event.wheel.x, event.wheel.y); });
break;
}
case SDL_EVENT_WINDOW_RESIZED:
Expand Down Expand Up @@ -143,7 +152,8 @@ void FrameLoop::Update(const float deltaTime) {
}
#endif

auto& options = registry_->Get<GameConfig>().GetEngineOptions();
auto& gameConfig = registry_->Get<GameConfig>();
auto& options = gameConfig.GetEngineOptions();

// Master volume + mute live at the mixer level so they apply to every track (including loops
// already playing). Synced every frame — and outside the pause gate — so toggling mute reacts
Expand Down Expand Up @@ -174,7 +184,11 @@ void FrameLoop::Update(const float deltaTime) {
#endif

if (!options.isPaused || options.stepFrame) {
registry_->Update(deltaTime * options.timeScale);
// The per-frame tick runs every game system, including ScriptSystem's Lua calls. In an editor
// session an uncaught exception here is caught + paused instead of crashing the editor; in the
// player/headless paths it propagates unchanged. See EditorExceptionGuard.h.
const float scaledDelta = deltaTime * options.timeScale;
octarine::editor::RunEditorGuarded(gameConfig, "Update", [&] { registry_->Update(scaledDelta); });
options.stepFrame = false;
} else {
// If paused, we might still want to clear some per-frame signals so they don't get stuck.
Expand Down
Loading