From b999fec59ee85702d7814b8625271174be717bfc Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Thu, 16 Jul 2026 12:02:30 +0100 Subject: [PATCH 1/8] vm: only check for modulo by zero for integers Fixes Deltarune Ch5 crashing when entering the garden of hopes and dreams --- src/vm.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vm.c b/src/vm.c index c7085f574..afd6c0855 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1550,9 +1550,14 @@ static void handleDiv(VMContext* ctx, uint32_t instr) { static void handleRem(VMContext* ctx, uint32_t instr) { RValue b = stackPop(ctx); RValue a = stackPop(ctx); - int64_t divisor = RValue_toInt64(b); - requireMessageFormatted(__FILE__, __LINE__, divisor != 0, "VM: [%s] DoRem :: Divide by zero", ctx->currentCodeName); - int64_t result = RValue_toInt64(a) / divisor; + uint8_t type1 = instrType1(instr); + uint8_t type2 = instrType2(instr); + GMLReal divisor = RValue_toReal(b); + // Ditto + if ((type1 == GML_TYPE_INT32 || type1 == GML_TYPE_INT64) && (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64)) { + requireMessageFormatted(__FILE__, __LINE__, divisor != 0.0, "VM: [%s] DoRem :: Divide by zero", ctx->currentCodeName); + } + GMLReal result = GMLReal_fmod(RValue_toReal(a), divisor); RValue_free(&a); RValue_free(&b); stackPushTyped(ctx, RValue_makeInt64(result), instrType2(instr)); From c348702253eeeed7bd6a875a139ae596eaed949b Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Thu, 16 Jul 2026 12:08:27 +0100 Subject: [PATCH 2/8] tile_set_mirror, tile_set_flip, tile_set_rotate --- src/vm_builtins.c | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/vm_builtins.c b/src/vm_builtins.c index ad61091e4..51ab215af 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -14086,14 +14086,14 @@ static RValue builtin_tile_get_index(MAYBE_UNUSED VMContext* ctx, RValue* args, // (see GameMaker-HTML5 Function_Layers.js) static RValue builtin_tile_get_mirror(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { if (1 > argCount) return RValue_makeBool(false); - return RValue_makeBool((RValue_toInt32(args[0]) & (1 << 28)) != 0); + return RValue_makeBool((RValue_toInt32(args[0]) & TILEMIRROR_MASK) != 0); } // tile_get_flip(tiledata): returns whether the vertical-flip bit is set on a raw tile cell value. // (see GameMaker-HTML5 Function_Layers.js) static RValue builtin_tile_get_flip(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { if (1 > argCount) return RValue_makeBool(false); - return RValue_makeBool((RValue_toInt32(args[0]) & (1 << 29)) != 0); + return RValue_makeBool((RValue_toInt32(args[0]) & TILEFLIP_MASK) != 0); } // tile_get_rotate(tiledata): returns whether the 90-degree-rotate bit is set on a raw tile cell value. @@ -14110,6 +14110,42 @@ static RValue builtin_tile_set_empty(MAYBE_UNUSED VMContext* ctx, RValue* args, return RValue_makeReal((GMLReal) (RValue_toInt32(args[0]) & ~TILEINDEX_SHIFTEDMASK)); } +// tile_set_mirror(tiledata): sets the horizontal-mirror bit on a raw tile cell value. +// (see GameMaker-HTML5 Function_Layers.js) +static RValue builtin_tile_set_mirror(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeReal(-1.0); + int32_t cell = RValue_toInt32(args[0]); + if (RValue_toBool(args[1])) + cell |= TILEMIRROR_MASK; + else + cell &= ~TILEMIRROR_MASK; + return RValue_makeReal((GMLReal) cell); +} + +// tile_set_flip(tiledata): sets the vertical-flip bit on a raw tile cell value. +// (see GameMaker-HTML5 Function_Layers.js) +static RValue builtin_tile_set_flip(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeReal(-1.0); + int32_t cell = RValue_toInt32(args[0]); + if (RValue_toBool(args[1])) + cell |= TILEFLIP_MASK; + else + cell &= ~TILEFLIP_MASK; + return RValue_makeReal((GMLReal) cell); +} + +// tile_set_rotate(tiledata): sets the 90-degree-rotate bit on a raw tile cell value. +// (see GameMaker-HTML5 Function_Layers.js) +static RValue builtin_tile_set_rotate(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + if (2 > argCount) return RValue_makeReal(-1.0); + int32_t cell = RValue_toInt32(args[0]); + if (RValue_toBool(args[1])) + cell |= TILEROTATE_MASK; + else + cell &= ~TILEROTATE_MASK; + return RValue_makeReal((GMLReal) cell); +} + static RValue builtin_layer_get_all(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { Runner* runner = ctx->runner; size_t count = arrlenu(runner->runtimeLayers); @@ -17079,6 +17115,9 @@ void VMBuiltins_registerAll(VMContext* ctx) { VM_registerBuiltin(ctx, "tile_get_flip", builtin_tile_get_flip); VM_registerBuiltin(ctx, "tile_get_rotate", builtin_tile_get_rotate); VM_registerBuiltin(ctx, "tile_set_empty", builtin_tile_set_empty); + VM_registerBuiltin(ctx, "tile_set_mirror", builtin_tile_set_mirror); + VM_registerBuiltin(ctx, "tile_set_flip", builtin_tile_set_flip); + VM_registerBuiltin(ctx, "tile_set_rotate", builtin_tile_set_rotate); VM_registerBuiltin(ctx, "tilemap_set", builtin_tilemap_set); VM_registerBuiltin(ctx, "tilemap_set_at_pixel", builtin_tilemap_set_at_pixel); #endif From 1bb2409ddc971b8464e65e29d81a712a3d322644 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Thu, 16 Jul 2026 12:34:08 +0100 Subject: [PATCH 3/8] why are we doing requireNotNull here again? fixes segfaults in some ch5 rooms --- src/vm_builtins.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 51ab215af..d33f0e357 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -13536,9 +13536,8 @@ static RValue builtin_layer_tile_visible(VMContext* ctx, RValue* args, MAYBE_UNU } static bool isValidLayerSpriteElement(RuntimeLayerElement* element) { - bool isValid = element != nullptr && element->type == RuntimeLayerElementType_Sprite; - requireNotNull(element->spriteElement); // If this crashes then something went DEEPLY wrong - return isValid; + return element != nullptr && element->spriteElement != nullptr && \ + element->type == RuntimeLayerElementType_Sprite; } static RValue builtin_layer_sprite_get_id(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { From a485eff0c1baa3cd5d9855d2a50f09bb1861291f Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 17 Jul 2026 11:35:29 +0100 Subject: [PATCH 4/8] draw_path fixes lines in ch2 cyber world background --- src/vm_builtins.c | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/vm_builtins.c b/src/vm_builtins.c index d33f0e357..647c634bb 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -14373,6 +14373,46 @@ static GamePath* getPath(Runner* runner, int32_t pathIdx) { return &runner->dataWin->path.paths[pathIdx]; } +// draw_path(index, x, y, absolute) - draws a path outline using the current draw color/alpha +static RValue builtin_draw_path(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { + Runner* runner = ctx->runner; + if (runner->renderer == nullptr) return RValue_makeUndefined(); + + int32_t index = RValue_toInt32(args[0]); + GMLReal x = RValue_toReal(args[1]); + GMLReal y = RValue_toReal(args[2]); + bool absolute = RValue_toBool(args[3]); + + GamePath* path = getPath(runner, index); + if (path == nullptr) return RValue_makeUndefined(); + + PathPositionResult origin = GamePath_getPosition(path, 0.0f); + + float xoff = absolute ? 0.0f : x - origin.x; + float yoff = absolute ? 0.0f : y - origin.y; + + if (runner->applyOffsetForPrimitives) { + xoff += 1.0f; + yoff += 1.0f; + } + + int maxsteps = (int) roundf(path->length / 4.0f); + if (maxsteps <= 0) return RValue_makeUndefined(); + + PathPositionResult prev = GamePath_getPosition(path, 0.0f); + for (int i = 1; i <= maxsteps; i++) { + float t = (float) i / (float) maxsteps; + PathPositionResult cur = GamePath_getPosition(path, t); + runner->renderer->vtable->drawLine(runner->renderer, + xoff + prev.x, yoff + prev.y, + xoff + cur.x, yoff + cur.y, + 1.0f, runner->renderer->drawColor, runner->renderer->drawAlpha); + prev = cur; + } + + return RValue_makeUndefined(); +} + // path_add() - create a new empty path, return its index static RValue builtin_path_add(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { Runner* runner = ctx->runner; @@ -16922,6 +16962,7 @@ void VMBuiltins_registerAll(VMContext* ctx) { VM_registerBuiltin(ctx, "draw_ellipse_color", builtin_draw_ellipse_color); VM_registerBuiltin(ctx, "draw_set_circle_precision", builtin_draw_set_circle_precision); VM_registerBuiltin(ctx, "draw_get_circle_precision", builtin_draw_get_circle_precision); + VM_registerBuiltin(ctx, "draw_path", builtin_draw_path); VM_registerBuiltin(ctx, "draw_set_colour", builtin_draw_set_colour); VM_registerBuiltin(ctx, "draw_get_colour", builtin_draw_get_colour); VM_registerBuiltin(ctx, "draw_get_color", builtin_draw_get_color); From b450b4184bb4d056458d0b345d4a31c650361d7b Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Fri, 17 Jul 2026 14:01:13 +0100 Subject: [PATCH 5/8] date_current_datetime --- src/vm_builtins.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 647c634bb..2c25ab4a2 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -7866,6 +7866,12 @@ static RValue builtin_window_get_cursor(VMContext* ctx, MAYBE_UNUSED RValue* arg return RValue_makeReal(runner->currentCursor); } +// ===[ Date Functions ]=== +static RValue builtin_date_current_datetime(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { + time_t now = time(NULL); + return RValue_makeReal((GMLReal)now / (GMLReal)86400 + (GMLReal)25569); +} + // ===[ Game State Functions ]=== static RValue builtin_game_restart(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { ctx->runner->pendingRoom = ROOM_RESTARTGAME; @@ -16793,6 +16799,9 @@ void VMBuiltins_registerAll(VMContext* ctx) { VM_registerBuiltin(ctx, "window_set_cursor", builtin_window_set_cursor); VM_registerBuiltin(ctx, "window_get_cursor", builtin_window_get_cursor); + // Date + VM_registerBuiltin(ctx, "date_current_datetime", builtin_date_current_datetime); + // Game VM_registerBuiltin(ctx, "game_restart", builtin_game_restart); VM_registerBuiltin(ctx, "game_end", builtin_game_end); From d1542149f7d83ccd069ea42f9a8db7af6afca3f8 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Sat, 25 Jul 2026 15:25:19 +0100 Subject: [PATCH 6/8] ugh --- src/vm_builtins.c | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/vm_builtins.c b/src/vm_builtins.c index 2c25ab4a2..36c94d51b 100644 --- a/src/vm_builtins.c +++ b/src/vm_builtins.c @@ -14091,14 +14091,14 @@ static RValue builtin_tile_get_index(MAYBE_UNUSED VMContext* ctx, RValue* args, // (see GameMaker-HTML5 Function_Layers.js) static RValue builtin_tile_get_mirror(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { if (1 > argCount) return RValue_makeBool(false); - return RValue_makeBool((RValue_toInt32(args[0]) & TILEMIRROR_MASK) != 0); + return RValue_makeBool((RValue_toInt32(args[0]) & (1 << 28)) != 0); } // tile_get_flip(tiledata): returns whether the vertical-flip bit is set on a raw tile cell value. // (see GameMaker-HTML5 Function_Layers.js) static RValue builtin_tile_get_flip(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { if (1 > argCount) return RValue_makeBool(false); - return RValue_makeBool((RValue_toInt32(args[0]) & TILEFLIP_MASK) != 0); + return RValue_makeBool((RValue_toInt32(args[0]) & (1 << 29)) != 0); } // tile_get_rotate(tiledata): returns whether the 90-degree-rotate bit is set on a raw tile cell value. @@ -14115,42 +14115,6 @@ static RValue builtin_tile_set_empty(MAYBE_UNUSED VMContext* ctx, RValue* args, return RValue_makeReal((GMLReal) (RValue_toInt32(args[0]) & ~TILEINDEX_SHIFTEDMASK)); } -// tile_set_mirror(tiledata): sets the horizontal-mirror bit on a raw tile cell value. -// (see GameMaker-HTML5 Function_Layers.js) -static RValue builtin_tile_set_mirror(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (2 > argCount) return RValue_makeReal(-1.0); - int32_t cell = RValue_toInt32(args[0]); - if (RValue_toBool(args[1])) - cell |= TILEMIRROR_MASK; - else - cell &= ~TILEMIRROR_MASK; - return RValue_makeReal((GMLReal) cell); -} - -// tile_set_flip(tiledata): sets the vertical-flip bit on a raw tile cell value. -// (see GameMaker-HTML5 Function_Layers.js) -static RValue builtin_tile_set_flip(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (2 > argCount) return RValue_makeReal(-1.0); - int32_t cell = RValue_toInt32(args[0]); - if (RValue_toBool(args[1])) - cell |= TILEFLIP_MASK; - else - cell &= ~TILEFLIP_MASK; - return RValue_makeReal((GMLReal) cell); -} - -// tile_set_rotate(tiledata): sets the 90-degree-rotate bit on a raw tile cell value. -// (see GameMaker-HTML5 Function_Layers.js) -static RValue builtin_tile_set_rotate(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) { - if (2 > argCount) return RValue_makeReal(-1.0); - int32_t cell = RValue_toInt32(args[0]); - if (RValue_toBool(args[1])) - cell |= TILEROTATE_MASK; - else - cell &= ~TILEROTATE_MASK; - return RValue_makeReal((GMLReal) cell); -} - static RValue builtin_layer_get_all(VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) { Runner* runner = ctx->runner; size_t count = arrlenu(runner->runtimeLayers); @@ -17164,9 +17128,6 @@ void VMBuiltins_registerAll(VMContext* ctx) { VM_registerBuiltin(ctx, "tile_get_flip", builtin_tile_get_flip); VM_registerBuiltin(ctx, "tile_get_rotate", builtin_tile_get_rotate); VM_registerBuiltin(ctx, "tile_set_empty", builtin_tile_set_empty); - VM_registerBuiltin(ctx, "tile_set_mirror", builtin_tile_set_mirror); - VM_registerBuiltin(ctx, "tile_set_flip", builtin_tile_set_flip); - VM_registerBuiltin(ctx, "tile_set_rotate", builtin_tile_set_rotate); VM_registerBuiltin(ctx, "tilemap_set", builtin_tilemap_set); VM_registerBuiltin(ctx, "tilemap_set_at_pixel", builtin_tilemap_set_at_pixel); #endif From f67c87ee9e316fd4a371eb08127a1e269941ea58 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Sun, 9 Aug 2026 11:11:44 +0100 Subject: [PATCH 7/8] fix(vm): correct REM opcode handler --- src/real_type.h | 2 ++ src/vm.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/real_type.h b/src/real_type.h index aeaff8a3f..2e7e4fae9 100644 --- a/src/real_type.h +++ b/src/real_type.h @@ -34,6 +34,7 @@ typedef float GMLReal; #define GMLReal_floor floorf #define GMLReal_ceil ceilf #define GMLReal_round roundf +#define GMLReal_trunc truncf #define GMLReal_pow powf #define GMLReal_log2 log2f #define GMLReal_fmax fmaxf @@ -58,6 +59,7 @@ typedef double GMLReal; #define GMLReal_floor floor #define GMLReal_ceil ceil #define GMLReal_round round +#define GMLReal_trunc trunc #define GMLReal_pow pow #define GMLReal_log2 log2 #define GMLReal_fmax fmax diff --git a/src/vm.c b/src/vm.c index 5c03a780b..e02098242 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1560,10 +1560,10 @@ static void handleRem(VMContext* ctx, uint32_t instr) { if ((type1 == GML_TYPE_INT32 || type1 == GML_TYPE_INT64) && (type2 == GML_TYPE_INT32 || type2 == GML_TYPE_INT64)) { requireMessageFormatted(__FILE__, __LINE__, divisor != 0.0, "VM: [%s] DoRem :: Divide by zero", ctx->currentCodeName); } - GMLReal result = GMLReal_fmod(RValue_toReal(a), divisor); + GMLReal result = GMLReal_trunc(RValue_toReal(a) / divisor); RValue_free(&a); RValue_free(&b); - stackPushTyped(ctx, RValue_makeInt64(result), instrType2(instr)); + stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); } static void handleMod(VMContext* ctx, uint32_t instr) { From 70c11e59de20d9823dfb8c32192b073845fd8703 Mon Sep 17 00:00:00 2001 From: cobaltgit Date: Sun, 9 Aug 2026 11:18:09 +0100 Subject: [PATCH 8/8] feat(compat): add trunc(f) for pre-GCC 3.0 --- compat/configure.sh | 24 ++++++++++++++++++++++++ src/math_compat.h | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/compat/configure.sh b/compat/configure.sh index 71f5e90a8..1fca87baf 100644 --- a/compat/configure.sh +++ b/compat/configure.sh @@ -284,6 +284,14 @@ int main(void){return round(0);} check '' round $lm > /dev/null & round_pid=$! +printf '%s' "\ +#include +int main(void){return trunc(0);} +" > tmp/trunc.c + +check '' trunc $lm > /dev/null & +trunc_pid=$! + printf '%s' "\ #include int main(void){return log2(1);} @@ -356,6 +364,14 @@ int main(void){return roundf(0);} check '' roundf $lm > /dev/null & roundf_pid=$! +printf '%s' "\ +#include +int main(void){return truncf(0);} +" > tmp/truncf.c + +check '' truncf $lm > /dev/null & +truncf_pid=$! + printf '%s' "\ #include int main(void){return isinf(0.0);} @@ -469,6 +485,10 @@ if ! checkend 'for round' "$round_pid"; then define 'NO_ROUND' fi +if ! checkend 'for trunc' "$truncf_pid"; then + define 'NO_TRUNC' +fi + if ! checkend 'for log2' "$log2_pid"; then define 'NO_LOG2' fi @@ -505,6 +525,10 @@ if ! checkend 'for roundf' "$roundf_pid"; then define 'NO_ROUNDF' fi +if ! checkend 'for truncf' "$truncf_pid"; then + define 'NO_TRUNCF' +fi + if ! checkend 'for isinf' "$isinf_pid"; then define 'NO_ISINF' fi diff --git a/src/math_compat.h b/src/math_compat.h index 09c42112a..b211f289c 100644 --- a/src/math_compat.h +++ b/src/math_compat.h @@ -43,6 +43,17 @@ static GMLReal GMLReal_round(GMLReal x) { #endif +#ifdef NO_TRUNC + +#undef GMLReal_trunc +static GMLReal GMLReal_trunc(GMLReal x) { + if (x >= 9007199254740992.0 || x <= -9007199254740992.0) return x; + if (x >= 0.0) return (GMLReal)((int64_t)x); + else return (GMLReal)((int64_t)x); +} + +#endif + #ifdef NO_LOG2 #undef GMLReal_log2 @@ -112,6 +123,16 @@ static float roundf(float x) { #endif +#ifdef NO_TRUNCF + +static float truncf(float x) { + if (x >= 2147483648.0f || x <= -2147483648.0f) return x; + if (x >= 0.0f) return (float)((int32_t)x); + else return (float)((int32_t)x); +} + +#endif + #ifndef M_PI #define M_PI 3.14159265358979323846 #endif