sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzAdd sdtin read Program wall With Claude
src/levels/level/tutorial.cpp | 9 +- src/states/game_state.cpp | 21 ++++- src/states/game_state.hpp | 2 + src/systems/program_system.cpp | 170 +++++++++++++++++++++++++++++++++----- src/systems/program_system.hpp | 28 +++++-- src/systems/projectile_system.cpp | 53 +++++++++++- src/systems/projectile_system.hpp | 5 +- 7 files changed, 253 insertions(+), 35 deletions(-) diff --git a/src/levels/level/tutorial.cpp b/src/levels/level/tutorial.cpp index 8c5c262..1237a03 100644 --- a/src/levels/level/tutorial.cpp +++ b/src/levels/level/tutorial.cpp @@ -19,7 +19,14 @@ Move the mouse to turn. A green plate sits on the floor a short walk away. Step onto it to continue.)", - .grants = { }, + .grants = { + Level::TokenGrant::give("#include <stdio.h>" ), + Level::TokenGrant::give("int main(void) {" ), + Level::TokenGrant::give("int in_;" ), + Level::TokenGrant::give("scanf(\"%d\", &in_);" ), + Level::TokenGrant::give("printf(\"%d\\n\", in_);"), + Level::TokenGrant::give("return 0; }" ), + }, .scene = Scene{ .words = {}, .plates = { diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp index cbd7741..b9e66da 100644 --- a/src/states/game_state.cpp +++ b/src/states/game_state.cpp @@ -442,6 +442,22 @@ GameState::on_TextCancel(FrameInput& input) } +void +GameState::program_FeedInput(ProjectileResult& result) +{ + std::string line; + for (const Text3dPtr& w : result.words) { + if (!w || w->type != ObjectType::Text) continue; + if (!line.empty()) line += ' '; + line += w->text; + } + if (!line.empty()) + _program_sys->feed_Input(line); + + _program_sys->echo_Words(_ctx.wall_texts, result.words, _ctx.world->scale()); +} + + void GameState::step_Spawn(const DynArray<std::string>& tokens) { @@ -970,7 +986,8 @@ GameState::substep(double dt) DynArray<ProjectileResult> proj_results; { FP_ZONE("subst.projectile_sys"); _projectiles_sys->update(_projectiles, proj_results, - wall_text_hits, dt, _ctx.world->scale()); + wall_text_hits, dt, _ctx.world->scale(), + _program_sys->is_Running()); } DynArray<size_t> wall_text_to_erase; @@ -979,6 +996,8 @@ GameState::substep(double dt) for (ProjectileResult& result : proj_results) { switch (result.landing) { case ProjectileLanding::Floor: projectile_LandOnFloor(result); break; + case ProjectileLanding::ProgramInput: + program_FeedInput(result); break; case ProjectileLanding::Wall: projectile_LandOnWall(result); break; case ProjectileLanding::WallText: if (EditorWall::is_LineNumberHit(result.wall_text_index)) { diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp index a61e501..b9b0e9e 100644 --- a/src/states/game_state.hpp +++ b/src/states/game_state.hpp @@ -137,6 +137,8 @@ class GameState : public IState void infowall_StepdetailsShow (); void walltext_HandleShootDown (ProjectileResult& result, DynArray<size_t>& to_erase); + void program_FeedInput(ProjectileResult& result); + void projectile_LandOnFloor (ProjectileResult& result); void projectile_LandOnWall (ProjectileResult& result); diff --git a/src/systems/program_system.cpp b/src/systems/program_system.cpp index 5c7bf68..80849b5 100644 --- a/src/systems/program_system.cpp +++ b/src/systems/program_system.cpp @@ -1,16 +1,41 @@ #include <SDL3/SDL.h> #include <algorithm> +#include <cerrno> +#include <fcntl.h> +#include <signal.h> #include <unistd.h> #include <sys/select.h> +#include <sys/wait.h> #include "systems/program_system.hpp" #include "text_3d/text_3d.hpp" #include "utils/ascii.hpp" +#include "utils/config.hpp" static const char* kProgramPath = "/tmp/game_prog"; +ProgramSystem::~ProgramSystem() { close_Child(); } + + +void +ProgramSystem::close_Child() +{ + if (_in_fd >= 0) { ::close(_in_fd); _in_fd = -1; } + if (_out_fd >= 0) { ::close(_out_fd); _out_fd = -1; } + if (_child > 0) { + int status; + if (waitpid(_child, &status, WNOHANG) == 0) { // still alive → kill it + kill(_child, SIGKILL); + waitpid(_child, &status, 0); + } + _child = -1; + } + _pending.clear(); +} + + void ProgramSystem::run(TextWallList& texts, float world_scale) { @@ -18,12 +43,12 @@ ProgramSystem::run(TextWallList& texts, float world_scale) texts.erase( std::remove_if(texts.begin(), texts.end(), [](const TextWallPtr& wt) { - return wt->wall_position == Wall::position_of(Wall::Role::Program); + return wt->role == Wall::Role::Program; }), texts.end()); _count = 0; - if (_pipe) { pclose(_pipe); _pipe = nullptr; } + close_Child(); // stop any previous run cleanly // Nothing built yet — tell the player to compile first. if (access(kProgramPath, X_OK) != 0) { @@ -31,38 +56,143 @@ ProgramSystem::run(TextWallList& texts, float world_scale) return; } - // stderr → stdout so runtime messages show up too. - _pipe = popen("LC_ALL=C /tmp/game_prog 2>&1", "r"); - if (!_pipe) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ProgramSystem: popen failed"); + // Writing to a pipe whose read end has closed must not kill the game. + signal(SIGPIPE, SIG_IGN); + + int in_pipe[2] { -1, -1 }; // parent writes [1] → child reads [0] (stdin) + int out_pipe[2] { -1, -1 }; // child writes [1] → parent reads [0] (stdout) + if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ProgramSystem: pipe() failed"); + return; + } + + pid_t pid = fork(); + if (pid < 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ProgramSystem: fork() failed"); + ::close(in_pipe[0]); ::close(in_pipe[1]); + ::close(out_pipe[0]); ::close(out_pipe[1]); + return; + } + + if (pid == 0) { + // ── Child ──────────────────────────────────────────────────────── + dup2(in_pipe[0], STDIN_FILENO); + dup2(out_pipe[1], STDOUT_FILENO); + dup2(out_pipe[1], STDERR_FILENO); + ::close(in_pipe[0]); ::close(in_pipe[1]); + ::close(out_pipe[0]); ::close(out_pipe[1]); + // stdbuf -oL -eL line-buffers the child so a prompt printed before it + // blocks on input still reaches the wall. LC_ALL=C for stable output. + execl("/bin/sh", "sh", "-c", + "LC_ALL=C stdbuf -oL -eL /tmp/game_prog", (char*)nullptr); + _exit(127); // exec failed + } + + // ── Parent ───────────────────────────────────────────────────────────── + ::close(in_pipe[0]); // we only write the child's stdin + ::close(out_pipe[1]); // we only read the child's stdout + _in_fd = in_pipe[1]; + _out_fd = out_pipe[0]; + _child = pid; + + // Non-blocking both ways so neither read nor write can stall the frame. + for (int fd : { _out_fd, _in_fd }) { + int fl = fcntl(fd, F_GETFL, 0); + if (fl != -1) fcntl(fd, F_SETFL, fl | O_NONBLOCK); + } } void ProgramSystem::update(TextWallList& texts, float world_scale) { - if (!_pipe) return; + if (_out_fd < 0) return; char buf[512]; - int fd = fileno(_pipe); - fd_set set; FD_ZERO(&set); FD_SET(fd, &set); + fd_set set; struct timeval tv{0, 0}; // poll, never block the frame - while (select(fd + 1, &set, nullptr, nullptr, &tv) > 0) { - if (!fgets(buf, sizeof(buf), _pipe)) { - // EOF — program finished. - pclose(_pipe); - _pipe = nullptr; - if (_count == 0) - push_Line(texts, "(no output)", world_scale); + for (;;) { + FD_ZERO(&set); FD_SET(_out_fd, &set); + int s = select(_out_fd + 1, &set, nullptr, nullptr, &tv); + if (s <= 0) break; // nothing ready this frame + + ssize_t n = read(_out_fd, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + _pending += buf; + + // Emit each complete line; keep any trailing partial for next read. + size_t nl; + while ((nl = _pending.find('\n')) != std::string::npos) { + std::string line = _pending.substr(0, nl); + _pending.erase(0, nl + 1); + if (!line.empty() && line.back() == '\r') line.pop_back(); + push_Line(texts, line, world_scale); + } + } + else if (n == 0) { + // EOF — program finished. Flush any partial line and reap. + if (!_pending.empty()) { + push_Line(texts, _pending, world_scale); + _pending.clear(); + } + if (_count == 0) push_Line(texts, "(no output)", world_scale); + close_Child(); return; } - std::string line(buf); - if (!line.empty() && line.back() == '\n') line.pop_back(); - if (!line.empty()) - push_Line(texts, line, world_scale); + else { + break; // EAGAIN / no data right now (or a read error) + } + } +} + + +void +ProgramSystem::feed_Input(const std::string& text) +{ + if (_in_fd < 0) return; // nothing is running + + std::string line = text; + line += '\n'; // terminate the line so scanf/cin/getline unblock + + size_t off = 0; + while (off < line.size()) { + ssize_t w = write(_in_fd, line.data() + off, line.size() - off); + if (w > 0) { off += static_cast<size_t>(w); continue; } + break; // EAGAIN (pipe full) or EPIPE (child closed stdin) → give up + } +} + + +void +ProgramSystem::echo_Words(TextWallList& texts, DynArray<Text3dPtr>& words, float world_scale) +{ + if (words.empty()) return; + + // Make room: scroll existing program output up by one line. + for (TextWallPtr& wt : texts) + if (wt->role == Wall::Role::Program) + wt->text3d->pos.y += static_cast<double>(line_height); + + Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, world_scale); - FD_ZERO(&set); FD_SET(fd, &set); + // The words have already flown to the bottom line; re-home them as Program + // wall text (dark, like output) and snap their row onto start_y. + for (Text3dPtr& w : words) { + if (!w) continue; + w->set_Color(ConfigColors::color_dark_gray); + w->yaw = tf.yaw; + w->pos.y = start_y; // align the echoed row to the line + w->set_Look(ConfigColors::color_keep, Text3d::depth_wall); + + texts.push_back(cpputils::make_unique<TextWall>(TextWall{ + Wall::position_of(Wall::Role::Program), + Wall::Role::Program, + std::move(w) + })); } + _count++; // counts as one printed line } diff --git a/src/systems/program_system.hpp b/src/systems/program_system.hpp index fc09e0c..dbf5be5 100644 --- a/src/systems/program_system.hpp +++ b/src/systems/program_system.hpp @@ -1,25 +1,37 @@ #pragma once #include <string> -#include <cstdio> +#include <sys/types.h> // pid_t #include "utils/globals.hpp" #include "wall/text_wall.hpp" // ProgramSystem: runs the executable built by CompilerSystem (/tmp/game_prog) -// and streams its stdout/stderr onto the Program wall (+X). Mirrors -// CompilerSystem's non-blocking pipe pattern so it never stalls the frame. +// and streams its stdout/stderr onto the Program wall (+X). The child is +// spawned with BOTH a readable stdout pipe and a writable stdin pipe, so the +// player can feed input to scanf/cin/getline by throwing words at the wall +// (see feed_Input). Polls non-blocking so it never stalls the frame. struct ProgramSystem { private: - FILE* _pipe {nullptr}; - int _count {0}; + int _out_fd {-1}; // read end of child's stdout+stderr + int _in_fd {-1}; // write end of child's stdin + pid_t _child {-1}; + int _count {0}; + std::string _pending; // partial-line buffer for stdout void push_Line(TextWallList&, const std::string& line, float world_scale); + void close_Child(); // close pipes, reap (or kill) the child public: static inline float line_height = 0.4f; static inline float start_y = -ConfigGame::world_box_scale + 1; - void run (TextWallList&, float world_scale); - void update (TextWallList&, float world_scale); - bool is_Running() const { return _pipe != nullptr; } + void run (TextWallList&, float world_scale); + void update (TextWallList&, float world_scale); + void feed_Input (const std::string& text); // thrown word(s) → child stdin + // Park already-thrown (animated) words on the Program wall as one echoed + // input line, scrolling existing output up — terminal style. + void echo_Words (TextWallList&, DynArray<Text3dPtr>& words, float world_scale); + bool is_Running () const { return _child > 0; } + + ~ProgramSystem(); }; diff --git a/src/systems/projectile_system.cpp b/src/systems/projectile_system.cpp index 2e5bc7a..24fe12e 100644 --- a/src/systems/projectile_system.cpp +++ b/src/systems/projectile_system.cpp @@ -103,7 +103,8 @@ tick_Sticking(Projectile& p, double dt, double ws, if (p.stick_t >= 1.0) { ProjectileResult r; - r.landing = ProjectileLanding::Wall; + r.landing = p.program_stick ? ProjectileLanding::ProgramInput + : ProjectileLanding::Wall; r.wall_pos = p.stick_wall; r.words = std::move(p.words); results.push_back(std::move(r)); @@ -270,6 +271,39 @@ begin_EditorStick(Projectile& p, Wall::Position hit_pos) } +// ── Helper: begin the program-wall "input echo" stick ──────────────────────── +// Like the editor stick, but aimed at the program wall's output line (centred, +// at the bottom row where new output appears) regardless of where it was hit — +// so a consumed input word glides into place like text typed at a terminal. +static void +begin_ProgramStick(Projectile& p, double ws) +{ + p.sticking = true; + p.program_stick = true; + p.stick_wall = Wall::Position::PosX; + p.vel = Vec3{0, 0, 0}; + p.stick_t = 0.0; + + const double cr_x = SDL_cos(p.yaw); + const double cr_z = -SDL_sin(p.yaw); + const double wt_z = -1.0; // PosX wall inward tangent + const double dot = cr_x * 0.0 + cr_z * wt_z; + const double sign = (dot >= 0.0) ? 1.0 : -1.0; + + const double c_tan = 0.0; // centred horizontally on the wall + const double c_y = -ws + 1.0; // matches ProgramSystem::start_y + + for (Text3dPtr& w : p.words) { + if (!w || w->type != ObjectType::Text) continue; + w->ribbon_local = { + c_tan + w->ribbon_local.x * sign, + c_y + w->ribbon_local.y, + 0.0 + }; + } +} + + // ── Helper: bounce a projectile off a non-editor wall ──────────────────────── // Returns true if the projectile should be removed (second bounce on this // axis → falls to the floor instead of bouncing again). @@ -311,8 +345,18 @@ static bool handle_WallHit(Projectile& p, const Vec3& centroid, Wall::Position hit_pos, double dt,double ws, const DynArray<WallTextHitTest>& wall_text_hits, - DynArray<ProjectileResult>& results) + DynArray<ProjectileResult>& results, + bool program_running) { + // Program wall (+X): a thrown word is terminal INPUT. + // - program running → it will be read: stick and glide to the output + // line (the "soaked" animation); GameState then feeds stdin + echoes it. + // - not running → nothing to read it: bounce off as normal. + if (Wall::role_of(hit_pos) == Wall::Role::Program) { + if (program_running) { begin_ProgramStick(p, ws); return false; } + return bounce_OffWall(p, hit_pos, ws, results); + } + // Wall-text intercept takes priority — but ONLY for a single-word shot, // which is what knocks an existing word down or hits a line number. A whole // shot LINE must never knock a word off; it always sticks (and merges) or @@ -391,7 +435,8 @@ void ProjectileSystem::update(DynArray<Projectile>& projectiles, DynArray<ProjectileResult>& results, const DynArray<WallTextHitTest>& wall_text_hits, - double dt, float world_scale) + double dt, float world_scale, + bool program_running) { const double ws = static_cast<double>(world_scale); @@ -421,7 +466,7 @@ ProjectileSystem::update(DynArray<Projectile>& projectiles, Wall::Position hit_pos{}; if (detect_WallHit(p, centroid, dt, ws, hit_pos)) remove = handle_WallHit(p, centroid, hit_pos, dt, ws, - wall_text_hits, results); + wall_text_hits, results, program_running); } if (!remove && !p.sticking && length(p.vel) < 1.0) { diff --git a/src/systems/projectile_system.hpp b/src/systems/projectile_system.hpp index e8153ae..a12bce0 100644 --- a/src/systems/projectile_system.hpp +++ b/src/systems/projectile_system.hpp @@ -20,6 +20,7 @@ struct Projectile { double yaw; uint8_t bounce_state = 0; bool sticking = false; + bool program_stick = false; // sticking to the Program wall as input echo double stick_t = 0.0; Wall::Position stick_wall { Wall::Position::PosX }; double step_dt = 0.0; @@ -43,6 +44,7 @@ enum class ProjectileLanding : uint8_t { Floor, Wall, WallText, + ProgramInput, }; struct ProjectileResult { @@ -64,5 +66,6 @@ public: void update(DynArray<Projectile>& projectiles, DynArray<ProjectileResult>& results, const DynArray<WallTextHitTest>& wall_text_hits, - double dt, float world_scale); + double dt, float world_scale, + bool program_running); };