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.gzPropper level compile in game With Claude
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; }
};