Files
EVEngine/src/engine/cmdline/Run.cpp
T

228 lines
8.8 KiB
C++

#include "cmdline.h"
#include "scripts.h"
#include "common/Module.h"
#include "common/Runtime.h"
#include "common/config.h"
#include "filesystem/Filesystem.h"
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
#include "devtools/DevTool.hpp"
#include "devtools/McpServer.hpp"
#endif
#include <simplesquirrel/simplesquirrel.hpp>
#include <CLI11.hpp>
#include <cstdint>
#include <string>
#include <filesystem>
#if defined(EVENGINE_WEBGPU)
#include <emscripten.h>
namespace {
// Global frame-loop state: the root script (load_web.nut) defines a global
// eve_frame() function; emscripten_set_main_loop drives it per animation frame.
ssq::VM* gFrameVm = nullptr;
ssq::Function* gFrameFunc = nullptr;
void webgpuFrameTick() {
if (!gFrameVm || !gFrameFunc || gFrameFunc->isEmpty()) return;
bool keep = true;
try {
ssq::Object r = gFrameVm->callFunc(*gFrameFunc, *gFrameVm);
if (r.getType() == ssq::Type::BOOL) keep = r.toBool();
} catch (const std::exception& e) {
fprintf(stderr, "EVEngine: eve_frame error: %s\n", e.what());
keep = false;
}
if (!keep) emscripten_cancel_main_loop();
}
} // namespace
#endif
#if defined(EVENGINE_ANDROID)
#include <android/log.h>
#define EVE_ANDROID_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "EVEngine", __VA_ARGS__)
#elif defined(EVENGINE_IOS) || defined(EVENGINE_WEBGPU)
#include <cstdio>
#define EVE_ANDROID_LOGE(...) do { fprintf(stderr, "EVEngine: "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while (0)
#else
#define EVE_ANDROID_LOGE(...) ((void)0)
#endif
using namespace std;
namespace eve::cmd
{
struct RunArgs : Handler {
std::string log_path, root_path;
bool no_window = false, debug = false;
int dap_port = 0;
int mcp_port = 0;
void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter) override {
auto run = app.add_subcommand("run", "Run game under current path");
run->allow_extras()->formatter(formatter);
run->add_flag("--no-window", no_window, "Run script only, no window mode");
run->add_flag("--debug", debug, "debug mode (slicer + pause/breakpoints/snapshot/MCP)");
run->add_option("--dap-port", dap_port,
"Start Debug Adapter Protocol server on port (implies --debug)");
run->add_option("--mcp-port", mcp_port,
"Start Model Context Protocol server on port for AI agents (implies --debug)");
run->add_option("-l,--log", log_path, "log messages into a file");
run->add_option("-r,--root", root_path, "give a entry script instead of using the system default one");
}
int parse(CLI::App& app, Cmdline& cmd) override {
auto run = app.get_subcommand("run");
if (run->parsed() || cmd.getArgc() == 1) {
if (dap_port > 0 || mcp_port > 0) debug = true;
std::string current_path = cmd.get_remaining(run);
if (root_path != "") {
ifstream ifs(root_path);
if (!ifs) {
cerr << "Cannot open root script: " << root_path << endl;
return -1;
}
std::string load_root((istreambuf_iterator<char>(ifs)), (istreambuf_iterator<char>()));
return cmd.Run(current_path, load_root, debug, dap_port, mcp_port);
} else {
#ifdef EVENGINE_WEBGPU
// The Emscripten runtime only wires the trimmed module set.
return cmd.Run(current_path, load_web_content, debug, dap_port, mcp_port);
#else
return cmd.Run(current_path, load_content, debug, dap_port, mcp_port);
#endif
}
}
return -1; // not handle
}
};
CMD_REG(RunArgs);
// create a new project
int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, int mcpPort) {
try {
// Switch to the game directory so load.nut can dofile("config.nut") / "main.nut".
if (!path.empty() && path != ".") {
std::error_code ec;
std::filesystem::current_path(path, ec);
if (ec) {
cerr << "Cannot chdir to game path '" << path << "': " << ec.message() << endl;
EVE_ANDROID_LOGE("Cannot chdir to game path '%s': %s", path.c_str(), ec.message().c_str());
return 2;
}
}
// Mount game dir for PhysFS so relative watch/read resolve (hot reload).
{
auto *fs = eve::filesystem::Filesystem::create();
if (fs) {
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
if (!ec) {
// setSource only succeeds once; ignore failure if already mounted.
fs->setSource(cwd.string());
}
}
}
Runtime runtime(2048, ssq::Libs::ALL);
runtime.initialize();
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
if (debug) {
auto& dt = eve::dev::DevTool::instance();
dt.attach(runtime.vm(), /*sampleLocals=*/true);
dt.exposeScriptApi(runtime.vm());
if (dapPort > 0) {
const int bound = dt.startDap(static_cast<uint16_t>(dapPort));
if (bound > 0) {
cerr << "DAP listening on 127.0.0.1:" << bound << endl;
// Wait for the IDE to finish setBreakpoints / configurationDone
// before loading scripts, otherwise early breakpoints are missed
// and stack paths are not registered yet.
if (!dt.dap().waitUntilConfigured(15000))
cerr << "DAP: timed out waiting for client; starting anyway" << endl;
} else {
cerr << "Failed to start DAP server on port " << dapPort << endl;
}
}
if (mcpPort > 0) {
try {
dt.mcp().setGameRoot(std::filesystem::current_path().string());
} catch (...) {
}
const int bound = dt.startMcp(static_cast<uint16_t>(mcpPort));
if (bound > 0) {
cerr << "MCP listening on 127.0.0.1:" << bound
<< " (newline JSON-RPC; use tools/eve-mcp for Cursor stdio)" << endl;
} else {
cerr << "Failed to start MCP server on port " << mcpPort << endl;
}
}
}
#else
(void)debug;
(void)dapPort;
(void)mcpPort;
#endif
// Embedded default demo (src/scripts/demo.nut); load.nut runs it when no main.nut.
{
ssq::Table eve = runtime.table("eve");
eve.set("demoScript", std::string(demo_content ? demo_content : ""));
eve.set("asyncScript", std::string(async_content ? async_content : ""));
}
// Name the embedded root so DAP stack frames map to load.nut (not "buffer").
runtime.runSource(root, "load.nut");
#if defined(EVENGINE_WEBGPU)
// Instead of a blocking while(running) Squirrel loop (which the browser
// never composites), drive the global eve_frame() function from an
// Emscripten requestAnimationFrame main loop. simulateInfiniteLoop=1
// keeps main() alive; `runtime` stays in scope because Run() never returns.
gFrameVm = &runtime.vm();
gFrameFunc = new ssq::Function(runtime.vm().find("eve_frame").toFunction());
emscripten_set_main_loop(&webgpuFrameTick, 0, /*simulateInfiniteLoop=*/1);
#endif
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
if (debug) eve::dev::DevTool::instance().detach();
#endif
return 0;
} catch (const std::exception& e) {
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
if (debug) {
const std::string report =
eve::dev::DevTool::instance().notifyError(e.what());
cerr << report << endl;
eve::dev::DevTool::instance().detach();
} else {
cerr << "Run failed: " << e.what() << endl;
}
#else
cerr << "Run failed: " << e.what() << endl;
#endif
EVE_ANDROID_LOGE("Run failed: %s", e.what());
return 3;
} catch (...) {
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
if (debug) {
const std::string report =
eve::dev::DevTool::instance().notifyError("unknown exception");
cerr << report << endl;
eve::dev::DevTool::instance().detach();
} else {
cerr << "Run failed: unknown exception" << endl;
}
#else
cerr << "Run failed: unknown exception" << endl;
#endif
EVE_ANDROID_LOGE("Run failed: unknown exception");
return 3;
}
}
} // namespace eve