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>
297 lines
12 KiB
C++
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
|