Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / 5a0009f

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

Propper level compile in game With Claude

Christian Immanuel · 2026-05-30 19:20 · 5a0009fb7c96efd2f9e3ed81f0672d31f4561783

 CMakeLists.txt                         |   2 +-
 src/{systems => assets}/game_mod_abi.h |   0
 src/assets/sandbox.c                   |  34 ++++++++
 src/assets/sandbox.c.last_ok           |  27 ++++++
 src/levels/level.hpp                   |   1 +
 src/levels/level_list.cpp              |  22 +----
 src/levels/level_runner.hpp            |   9 +-
 src/states/game_state.cpp              |  90 ++++++++++++++++++--
 src/states/game_state.hpp              |   5 ++
 src/systems/mod_loader.cpp             |   6 +-
 src/systems/mod_loader.hpp             |   4 +-
 src/systems/source_tokens.hpp          | 145 +++++++++++++++++++++++++++++++++
 src/wall/editor_wall.hpp               |   2 +
 13 files changed, 310 insertions(+), 37 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7b138ec..076d1a9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -138,7 +138,7 @@ add_custom_target(compile_commands_link ALL
 add_custom_command(TARGET SDL3_RuntimeCompiler POST_BUILD
     COMMAND ${CMAKE_COMMAND} -E make_directory /tmp/game_mod_include
     COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_SOURCE_DIR}/src/systems/game_mod_abi.h
+        ${CMAKE_SOURCE_DIR}/src/assets/game_mod_abi.h
         /tmp/game_mod_include/game_mod_abi.h)
 
 # ─────────────────────────────────────────────────────────────────────────────
diff --git a/src/systems/game_mod_abi.h b/src/assets/game_mod_abi.h
similarity index 100%
rename from src/systems/game_mod_abi.h
rename to src/assets/game_mod_abi.h
diff --git a/src/assets/sandbox.c b/src/assets/sandbox.c
new file mode 100644
index 0000000..b9ce2d8
--- /dev/null
+++ b/src/assets/sandbox.c
@@ -0,0 +1,34 @@
+#include "game_mod_abi.h"
+
+static int n;
+static ModObject* o;
+
+static void box(double x, double y, double z, double hx, double hy, double hz) {
+    o[n].kind = MOD_BOX;
+    o[n].x = x; o[n].y = y; o[n].z = z;
+    o[n].hx = hx; o[n].hy = hy; o[n].hz = hz;
+    o[n].r = 0.6; o[n].g = 0.6; o[n].b = 0.8;
+    n = n + 1;
+}
+
+static void text(double x, double y, double z, char* t) {
+    o[n].kind = MOD_TEXT;
+    o[n].x = x; o[n].y = y; o[n].z = z;
+    o[n].r = 1; o[n].g = 1; o[n].b = 1;
+    int i = 0;
+    while (t[i] != 0) { o[n].text[i] = t[i]; i = i + 1; }
+    o[n].text[i] = 0;
+    n = n + 1;
+}
+
+int game_build(ModObject* out, int max) {
+    (void)max;
+    o = out; n = 0;
+    box (0, -16, 5, 1, 1, 1);
+    text(0, -15, 5, "hi");
+    box (3, -16, 5, 1, 2, 1);
+
+
+
+    return n;
+}
diff --git a/src/assets/sandbox.c.last_ok b/src/assets/sandbox.c.last_ok
new file mode 100644
index 0000000..a6049fc
--- /dev/null
+++ b/src/assets/sandbox.c.last_ok
@@ -0,0 +1,27 @@
+# include "game_mod_abi.h"
+static int n ;
+static ModObject * o ;
+static void box ( double x , double y , double z , double hx , double hy , double hz ) {
+o [ n ] .kind = MOD_BOX ;
+o [ n ] .x = x ; o [ n ] .y = y ; o [ n ] .z = z ;
+o [ n ] .hx = hx ; o [ n ] .hy = hy ; o [ n ] .hz = hz ;
+o [ n ] .r = 0.6 ; o [ n ] .g = 0.6 ; o [ n ] .b = 0.8 ;
+n = n + 1 ;
+}
+static void text ( double x , double y , double z , char * t ) {
+o [ n ] .kind = MOD_TEXT ;
+o [ n ] .x = x ; o [ n ] .y = y ; o [ n ] .z = z ;
+o [ n ] .r = 1 ; o [ n ] .g = 1 ; o [ n ] .b = 1 ;
+int i = 0 ;
+while ( t [ i ] != 0 ) { o [ n ] .text [ i ] = t [ i ] ; i = i + 1 ; }
+o [ n ] .text [ i ] = 0 ;
+n = n + 1 ;
+}
+int game_build ( ModObject * out , int max ) {
+( void ) max ;
+o = out ; n = 0 ;
+box ( 0 , - 16 , 5 , 1 , 1 , 1 ) ;
+text ( 0 , - 15 , 5 , "hi" ) ;
+box ( 3 , - 16 , 5 , 1 , 2 , 1 ) ;
+return n ;
+}
diff --git a/src/levels/level.hpp b/src/levels/level.hpp
index 79fb7b1..d3ccda5 100644
--- a/src/levels/level.hpp
+++ b/src/levels/level.hpp
@@ -34,5 +34,6 @@ struct Level {
     std::string name;
     std::string desc;
     bool        dynamic_lib = false;
+    std::string source_file = {};
     std::vector<LevelStep> steps;
 };
diff --git a/src/levels/level_list.cpp b/src/levels/level_list.cpp
index f2fbf86..31b2549 100644
--- a/src/levels/level_list.cpp
+++ b/src/levels/level_list.cpp
@@ -429,6 +429,7 @@ Place all three lines BEFORE the existing return.)",
         .name = "Sandbox",
         .desc = "Build the world yourself: compile a library that spawns objects.",
         .dynamic_lib = true,
+        .source_file = "src/assets/sandbox.c",
         .steps = {
             {
                 .hint    = "Spawn a box with box(x,y,z, hx,hy,hz);",
@@ -445,26 +446,7 @@ Build something like:
   box(0, 5, -3,  1, 1, 1);
 
 Then press the Compile pult. Edit and recompile freely.)",
-                .grants = {
-                    TokenGrant::give("box"),
-                    TokenGrant::give("text"),
-                    TokenGrant::give("("),
-                    TokenGrant::give(")"),
-                    TokenGrant::give(","),
-                    TokenGrant::give(","),
-                    TokenGrant::give(","),
-                    TokenGrant::give(","),
-                    TokenGrant::give(";"),
-                    TokenGrant::give("0.0"),
-                    TokenGrant::give("1.0"),
-                    TokenGrant::give("2.0"),
-                    TokenGrant::give("3.0"),
-                    TokenGrant::give("5.0"),
-                    TokenGrant::give("-3.0"),
-                    TokenGrant::give("box(0,-16,-3,1,1,1);"),
-                    TokenGrant::give("\"hi\""),
-                    TokenGrant::give("\"world\""),
-                },
+                .grants = { },
                 .scene        = {},
                 .reset_carry  = true,
                 .goal         = AllOf{},
diff --git a/src/levels/level_runner.hpp b/src/levels/level_runner.hpp
index be8d454..fcbff14 100644
--- a/src/levels/level_runner.hpp
+++ b/src/levels/level_runner.hpp
@@ -9,10 +9,11 @@ public:
     // Returns true if there are more steps, false if level complete
     bool advance();
 
-    bool             complete() const { return current_step >= level->steps.size(); }
-    size_t           step()     const { return current_step; }
-    const LevelStep& current()  const { return level->steps[current_step]; }
-    bool             dynamic_lib() const { return level->dynamic_lib; }
+    bool               complete()    const { return current_step >= level->steps.size(); }
+    size_t             step()        const { return current_step; }
+    const LevelStep&   current()     const { return level->steps[current_step]; }
+    bool               dynamic_lib() const { return level->dynamic_lib; }
+    const std::string& source_file() const { return level->source_file; }
 
     // All tokens to spawn for the current step (resolved — no choices yet)
     // Choices are returned as multiple Text3d objects placed near each other
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index daf8fcf..0ec8b41 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -3,6 +3,7 @@
 #include <unordered_set>
 
 
+#include "assets/game_mod_abi.h"
 #include "core/game.hpp"
 #include "entities/solid_box.hpp"
 #include "entities/world.hpp"
@@ -10,10 +11,10 @@
 #include "levels/level_check.hpp"
 #include "levels/level_list.hpp"
 #include "rendering/gl_renderer.hpp"
-#include "systems/game_mod_abi.h"
 #include "systems/input_system.hpp"
 #include "systems/physics_system.hpp"
 #include "systems/pick_system.hpp"
+#include "systems/source_tokens.hpp"
 #include "text_3d/text_label.hpp"
 #include "utils/config.hpp"
 #include "utils/frame_profiler.hpp"
@@ -61,6 +62,8 @@ GameState::GameState(size_t starting_level)
     if (level_runner && !level_runner->complete())
         hint_sys->set_Hint(level_runner->current().hint);
 
+    if (level_runner && level_runner->dynamic_lib())
+        sandbox_LoadFileToWall();
 
     /*
     ctx.scenery.push_back(
@@ -131,13 +134,15 @@ GameState::pult_DispatchAction(PultAction act)
     switch (act) {
         case PultAction::ScrollEditorUp:
             TextWallScroll::scroll_Up(ctx.wall_texts, Wall::Role::Editor);
+            editor_wall.scroll(TextWallScroll::SCROLL_STEP);
             break;
         case PultAction::ScrollEditorDown:
             TextWallScroll::scroll_Down(ctx.wall_texts, Wall::Role::Editor);
+            editor_wall.scroll(-TextWallScroll::SCROLL_STEP);
             break;
         case PultAction::ClearEditor:
-            TextWallScroll::clear_ToFloor(ctx.wall_texts, ctx.words,
-                                          Wall::Role::Editor, floor_y);
+            TextWallScroll::clear_ToFloor(ctx.wall_texts, ctx.words, Wall::Role::Editor, floor_y);
+            editor_wall.clear();
             break;
         case PultAction::Compile:
             on_Compile();
@@ -300,13 +305,17 @@ GameState::on_ShootAll()
 }
 
 
-void
-GameState::on_Compile()
-{
+void GameState::on_Compile() {
     std::string src = TextWall::extract_Text(ctx.wall_texts, Wall::Role::Editor);
 
     if (level_runner && !level_runner->complete() && level_runner->dynamic_lib()) {
-        mod_loader->build(ctx.wall_texts, src, ctx.world->scale());
+        const std::string path = sandbox_CanonicalPath();
+        std::string stripped;
+        for (char c : src) if (!std::isspace((unsigned char)c)) { stripped = src; break; }
+        if (!path.empty() && !stripped.empty())
+            SourceTokens::write_File(path, src);
+
+        mod_loader->build(ctx.wall_texts, src, ctx.world->scale(), true);
     } else {
         compiler_sys->compile(ctx.wall_texts, src);
     }
@@ -549,6 +558,7 @@ GameState::update(double dt)
         if (mod_loader->last_build_ok) {
             mod_loader->last_build_ok = false;
             mod_Apply(mod_loader->load_Objects());
+            SourceTokens::copy_File(sandbox_CanonicalPath(), sandbox_BackupPath());
         }
     }
 
@@ -1137,3 +1147,69 @@ GameState::mod_Apply(const std::vector<ModObject>& objs)
         }
     }
 }
+
+std::string GameState::sandbox_CanonicalPath() const {
+    return level_runner ? level_runner->source_file() : std::string{};
+}
+std::string GameState::sandbox_BackupPath() const {
+    std::string c = sandbox_CanonicalPath();
+    return c.empty() ? c : (c + ".last_ok");
+}
+
+void
+GameState::sandbox_LoadFileToWall()
+{
+    const std::string path = sandbox_CanonicalPath();
+    if (path.empty()) return;
+
+    std::string src;
+    if (!SourceTokens::read_File(path, src)) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+                     "Sandbox: cannot read %s", path.c_str());
+        return;
+    }
+
+    // Clear any existing editor-wall words first.
+    editor_wall.clear();
+    ctx.wall_texts.erase(
+        std::remove_if(ctx.wall_texts.begin(), ctx.wall_texts.end(),
+            [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }),
+        ctx.wall_texts.end());
+
+    const float ws = ctx.world->scale();
+    const double x_wall = -static_cast<double>(ws) + 0.05;   // NegX wall surface
+    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
+
+    auto lines = SourceTokens::tokenize_Lines(src);
+
+    // Top line highest; step down by the editor's line height.
+    double y = static_cast<double>(ws) - 2.0;   // start near top, tweak to taste
+    const double z_start = static_cast<double>(ws) - 1.0;   // leftmost (large z)
+
+    for (const auto& toks : lines) {
+        double z = z_start;
+        for (const std::string& tk : toks) {
+            Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f,
+                                      Vec3{ x_wall, y, z }, Text3d::color_wall);
+            t->yaw = tf.yaw;
+
+            // Snap into the editor wall's line tracking, like a thrown word.
+            Vec3 snapped = editor_wall.on_WordPlaced(t.get(), Vec3{ x_wall, y, z });
+            t->pos = snapped;
+
+            ctx.wall_texts.push_back(cpputils::make_unique<TextWall>(TextWall{
+                Wall::Position::NegX, Wall::Role::Editor, std::move(t)
+            }));
+
+            // Advance left for the next token. on_WordPlaced already spaced
+            // it; nudge our cursor past where it landed.
+            z = snapped.z - 0.5;   // coarse step; place_OnLine handles exact gaps
+        }
+        y -= EditorWall::LINE_HEIGHT;
+    }
+}
+
+void GameState::sandbox_Restore() {
+    if (SourceTokens::copy_File(sandbox_BackupPath(), sandbox_CanonicalPath()))
+        sandbox_LoadFileToWall();
+}
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index 5fab661..639837a 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -129,7 +129,12 @@ class GameState : public IState
     void infowall_Clear();
     void infowall_StepdetailsShow();
 
+    // dynamic-lib sandbox
     void mod_Apply(const std::vector<ModObject>& objs);
+    void sandbox_LoadFileToWall();   // tokenize source_file → editor wall
+    void sandbox_Restore();          // copy backup → canonical, reload wall
+    std::string sandbox_CanonicalPath() const;
+    std::string sandbox_BackupPath()    const;
 
     bool        level_TryAdvance();
     void        level_Advance();
diff --git a/src/systems/mod_loader.cpp b/src/systems/mod_loader.cpp
index f3e5c59..4582c84 100644
--- a/src/systems/mod_loader.cpp
+++ b/src/systems/mod_loader.cpp
@@ -62,7 +62,7 @@ ModLoader::wrap_Source(const std::string& user_body)
 
 
 void
-ModLoader::build(TextWallList& texts, const std::string& user_src, float world_scale)
+ModLoader::build(TextWallList& texts, const std::string& user_src, float world_scale, bool full_source)
 {
     last_build_ok = false;
 
@@ -83,8 +83,8 @@ ModLoader::build(TextWallList& texts, const std::string& user_src, float world_s
         push_Line(texts, "internal error: cannot write source", world_scale);
         return;
     }
-    const std::string wrapped = wrap_Source(user_src);
-    fputs(wrapped.c_str(), f);
+    const std::string out = full_source ? user_src : wrap_Source(user_src);
+    fputs(out.c_str(), f);
     fclose(f);
 
     // Unique output path each build — dlopen() caches handles by path, so a
diff --git a/src/systems/mod_loader.hpp b/src/systems/mod_loader.hpp
index f4aff09..048149e 100644
--- a/src/systems/mod_loader.hpp
+++ b/src/systems/mod_loader.hpp
@@ -5,12 +5,12 @@
 
 #include "wall/text_wall.hpp"
 
-#include "systems/game_mod_abi.h"
+#include "assets/game_mod_abi.h"
 
 class ModLoader
 {
 public:
-    void build(TextWallList& texts, const std::string& user_src, float world_scale);
+    void build(TextWallList& texts, const std::string& user_src, float world_scale, bool full_source);
     void update(TextWallList& texts, float world_scale);
     bool is_Running() const { return pipe_ != nullptr; }
     bool last_build_ok { false };
diff --git a/src/systems/source_tokens.hpp b/src/systems/source_tokens.hpp
new file mode 100644
index 0000000..9ff6b7b
--- /dev/null
+++ b/src/systems/source_tokens.hpp
@@ -0,0 +1,145 @@
+#pragma once
+#include <string>
+#include <vector>
+#include <cctype>
+#include <fstream>
+#include <sstream>
+
+// ─────────────────────────────────────────────────────────────────────────
+// SourceTokens — Option 1 tokenizer + file IO for the dynamic-lib sandbox.
+//
+// Tokenizing rule (matches the editor-wall "one word per token" model):
+//   - identifier/number runs (alnum, '.', '_') stay whole:  fuu  1.0  my_var
+//   - a quoted "..." string is one token (quotes kept, \" respected)
+//   - every other character is its own single-char token:    ( ) , ; # * { }
+//
+// Round-trips to valid C when rejoined with single spaces (verified: even
+// `# include "x.h"` and unary `- 3` compile). The editor wall stores each
+// token as a Text3d word; extract_Text rejoins them with spaces, so the
+// file written back on compile is space-normalized but compilable.
+// ─────────────────────────────────────────────────────────────────────────
+namespace SourceTokens
+{
+
+inline std::vector<std::string>
+tokenize(const std::string& src)
+{
+    std::vector<std::string> out;
+    const size_t n = src.size();
+    size_t i = 0;
+    while (i < n) {
+        const char c = src[i];
+
+        if (std::isspace(static_cast<unsigned char>(c))) { ++i; continue; }
+
+        // String literal — consume through the closing quote.
+        if (c == '"') {
+            std::string s;
+            s += c;
+            ++i;
+            while (i < n) {
+                s += src[i];
+                const bool closing = (src[i] == '"' && src[i - 1] != '\\');
+                ++i;
+                if (closing) break;
+            }
+            out.push_back(std::move(s));
+            continue;
+        }
+
+        // Identifier / number run.
+        if (std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_') {
+            std::string s;
+            while (i < n) {
+                const char d = src[i];
+                if (std::isalnum(static_cast<unsigned char>(d)) || d == '.' || d == '_') {
+                    s += d;
+                    ++i;
+                } else {
+                    break;
+                }
+            }
+            out.push_back(std::move(s));
+            continue;
+        }
+
+        // Two-char operators: keep them whole so C expressions survive the
+        // round-trip (!=, ==, <=, >=, &&, ||, ++, --, +=, -=, *=, /=, ->, <<, >>).
+        if (i + 1 < n) {
+            const char d = src[i + 1];
+            const bool two =
+                (c == '!' && d == '=') ||
+                (c == '=' && d == '=') ||
+                (c == '<' && d == '=') ||
+                (c == '>' && d == '=') ||
+                (c == '&' && d == '&') ||
+                (c == '|' && d == '|') ||
+                (c == '+' && d == '+') ||
+                (c == '-' && d == '-') ||
+                (c == '+' && d == '=') ||
+                (c == '-' && d == '=') ||
+                (c == '*' && d == '=') ||
+                (c == '/' && d == '=') ||
+                (c == '-' && d == '>') ||
+                (c == '<' && d == '<') ||
+                (c == '>' && d == '>');
+            if (two) {
+                out.push_back(std::string{c, d});
+                i += 2;
+                continue;
+            }
+        }
+
+        // Any other character: single-char token.
+        out.push_back(std::string(1, c));
+        ++i;
+    }
+    return out;
+}
+
+// Tokenize per source LINE, preserving line breaks. Returns one vector of
+// tokens per non-empty line of the file, so the wall layout can mirror the
+// file's line structure (one editor row per source line).
+inline std::vector<std::vector<std::string>>
+tokenize_Lines(const std::string& src)
+{
+    std::vector<std::vector<std::string>> lines;
+    std::istringstream in(src);
+    std::string line;
+    while (std::getline(in, line)) {
+        std::vector<std::string> toks = tokenize(line);
+        if (!toks.empty())
+            lines.push_back(std::move(toks));
+    }
+    return lines;
+}
+
+inline bool
+read_File(const std::string& path, std::string& out)
+{
+    std::ifstream f(path, std::ios::binary);
+    if (!f) return false;
+    std::ostringstream ss;
+    ss << f.rdbuf();
+    out = ss.str();
+    return true;
+}
+
+inline bool
+write_File(const std::string& path, const std::string& contents)
+{
+    std::ofstream f(path, std::ios::binary | std::ios::trunc);
+    if (!f) return false;
+    f << contents;
+    return static_cast<bool>(f);
+}
+
+inline bool
+copy_File(const std::string& from, const std::string& to)
+{
+    std::string data;
+    if (!read_File(from, data)) return false;
+    return write_File(to, data);
+}
+
+}  // namespace SourceTokens
diff --git a/src/wall/editor_wall.hpp b/src/wall/editor_wall.hpp
index 183d56e..d290a50 100644
--- a/src/wall/editor_wall.hpp
+++ b/src/wall/editor_wall.hpp
@@ -79,4 +79,6 @@ public:
 
     // Diagnostics.
     size_t line_Count() const { return lines_.size(); }
+
+    void scroll(double delta) { for (Line& l : lines_) l.y += delta; }
 };