Files
EVEngine/src/engine/cmdline/Run.cpp
T
Cursor Agentand西风逍遥游 a97f7f281f feat(boot): bind whatever modules the build contains, and drop the forked root script
load.nut constructed about thirty modules by name, so any build that trimmed
one crashed at bind time. That is the only reason the browser runtime needed a
second root script, load_web.nut, listing the WASM-surviving subset by hand:
two files to keep in step with the link list, and no way to express any other
subset.

The boot script now walks eve.moduleList, generated at configure time from the
same manifest that produces the link list, and guards optional modules with
has_module(). The two frame drivers share one eve_frame(): desktop runs the
while loop at the bottom of load.nut, the browser leaves it to
emscripten_set_main_loop through the new eve.hostDrivesFrames flag rather than
a separate script. load_web.nut is gone.

EVENGINE_WEBGPU_TRIM is gone as well. What was a special case is now the "web"
profile, and the two remaining uses that really meant "is this Emscripten" say
so directly. Third-party groups the WASM aggregate does not build resolve to
nothing there, matching the list it linked before.

graphics now treats font as an optional dependency rather than a hard one:
graphics/Font.cpp is its only user and was already excluded on WASM. Turning
font off excludes that file everywhere, so the rule no longer depends on the
platform.

Verified: full-profile link line still identical to before the manifest work;
web profile resolves to exactly the module set the old REMOVE_ITEM list left;
a 3d-profile binary boots and runs the basic example, reporting avatar and
dialogue absent and the rest present; example smoke suite unchanged at 19/9.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
2026-08-18 13:46:25 +00:00

297 lines
12 KiB
C++

#include "cmdline.h"
#include "scripts.h"
#include "common/Module.h"
#include "common/Runtime.h"
#include "common/config.h"
#include "filesystem/Filesystem.h"
#include "filesystem/physfs/FileApi.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 <vector>
#include <filesystem>
#if defined(EVENGINE_WEBGPU)
#include <emscripten.h>
namespace {
// Global frame-loop state: the root script (load.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;
// First frame is presented by the browser, so the shell's loading overlay
// can be dismissed now (it covered the canvas until this point).
EM_ASM({ if (window.hideEVELoading) window.hideEVELoading(); });
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, dev_server;
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("--dev-server", dev_server,
"Remote hot-reload dev server URL (eve dev), e.g. http://192.168.1.5:8765");
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, dev_server);
} else {
// One root script for every platform: it binds whatever modules
// the build contains from eve.moduleList, and leaves the frame
// driver to eve.hostDrivesFrames.
return cmd.Run(current_path, load_content, debug, dap_port, mcp_port, dev_server);
}
}
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,
std::string devServer) {
try {
// Resolve the game directory. A packaged game ships a game.eve archive next to
// the executable; we mount it into memory and run without extracting to disk.
std::string gameDir = path;
std::string archivePath;
{
std::error_code ec;
if (path.empty() || path == ".")
gameDir = std::filesystem::current_path(ec).string();
else
gameDir = std::filesystem::absolute(path, ec).string();
if (ec) gameDir = path;
std::filesystem::path gp(gameDir);
if (std::filesystem::is_regular_file(gp, ec) && gp.extension() == ".eve") {
// `eve run <path>.eve` — the archive itself is the game.
archivePath = gp.string();
gameDir = gp.parent_path().string();
} else {
std::filesystem::path bundled = gp / "game.eve";
if (std::filesystem::is_regular_file(bundled, ec))
archivePath = bundled.string();
}
}
// Switch to the game directory so relative plugin/asset paths resolve next to
// the executable (the packaged game's scripts live in the memory-mounted archive).
if (!gameDir.empty() && gameDir != ".") {
std::error_code ec;
std::filesystem::current_path(gameDir, ec);
if (ec) {
cerr << "Cannot chdir to game path '" << gameDir << "': " << ec.message() << endl;
EVE_ANDROID_LOGE("Cannot chdir to game path '%s': %s", gameDir.c_str(), ec.message().c_str());
return 2;
}
}
// Mount game source for PhysFS so relative watch/read resolve (hot reload).
{
auto *fs = eve::filesystem::Filesystem::create();
if (fs) {
if (!archivePath.empty()) {
// Read the packaged archive into memory and mount it there.
std::ifstream ifs(archivePath, std::ios::binary);
if (!ifs) {
cerr << "Cannot open game archive: " << archivePath << endl;
return 2;
}
std::vector<char> bytes((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
if (bytes.empty() || !fs->setSourceFromMemory(bytes.data(), bytes.size())) {
cerr << "Failed to mount game archive from memory: " << archivePath << endl;
return 2;
}
} else {
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 : ""));
// Scene-director authoring kit (src/scripts/scene_director.nut). Host
// games load it via `compilestring(eve.sceneDirectorScript)()`; the
// MCP tools auto-install it on demand.
eve.set("sceneDirectorScript", std::string(scene_director_content ? scene_director_content : ""));
eve.set("devServerArg", devServer);
#if defined(EVENGINE_WEBGPU)
// The browser has no blocking loop; load.nut defines eve_frame and
// returns, and emscripten_set_main_loop drives it below.
eve.set("hostDrivesFrames", true);
#else
eve.set("hostDrivesFrames", false);
#endif
}
// The generated slot -> class table load.nut iterates over.
if (module_list_content && *module_list_content)
runtime.runSource(module_list_content, "module_list.nut");
{
ssq::Table eve = runtime.table("eve");
eve.set("moduleList", runtime.root().find("eve_modules"));
}
// Name the embedded root so DAP stack frames map to load.nut (not "buffer").
// Route file/dofile/loadfile through PhysFS so a packaged game (mounted in
// memory) can load its scripts without extracting to disk.
eve::filesystem::physfs::installScriptFileApi(runtime.vm());
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