Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / ee7b6e4

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz

Add sdtin read Program wall With Claude

Christian Immanuel · 2026-06-03 20:05 · ee7b6e470476c2bbb3c6257c7a74bfe742cfcc5c

 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);
 };