Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 219 additions & 49 deletions src/vm_builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -4136,7 +4136,14 @@ static RValue builtin_ds_map_create(VMContext* ctx, MAYBE_UNUSED RValue* args, M
return RValue_makeReal((GMLReal) dsMapCreate(runner));
}

static RValue builtin_ds_map_add(VMContext* ctx, RValue* args, int32_t argCount) {
static RValue makeMapListContainer(VMContext* ctx, int32_t id, int32_t type) {
Instance* container = Runner_createStruct(ctx->runner);
VM_structSetAndFreeVal(ctx, container, "ObjType", RValue_makeInt32(type), -1);
VM_structSetAndFreeVal(ctx, container, "Object", RValue_makeInt32(id), -1);
return RValue_makeStructAndIncRef(container);
}

static RValue dsMapAddCommon(VMContext* ctx, RValue* args, int32_t argCount, bool wrapAsContainer, int32_t containerType) {
if (3 > argCount) return RValue_makeUndefined();
Runner* runner = ctx->runner;
int32_t id = RValue_toInt32(args[0]);
Expand All @@ -4145,18 +4152,82 @@ static RValue builtin_ds_map_add(VMContext* ctx, RValue* args, int32_t argCount)

char* key = RValue_toString(args[1]);

// Only add if key doesn't exist
bool exists = shgeti(*mapPtr, key) != -1;
RValue valueToStore;
if (wrapAsContainer) {
// Wrap the value (ds_map or ds_list ID) in a container struct
int32_t containedId = RValue_toInt32(args[2]);
valueToStore = makeMapListContainer(ctx, containedId, containerType);
} else {
// Store the value directly
valueToStore = RValue_makeIndependent(args[2]);
}

if (exists) {
free(key); // Key already exists, we didn't insert it
// Check if key exists
ptrdiff_t existingIdx = shgeti(*mapPtr, key);
if (existingIdx != -1) {
// Key already exists - replace the value
RValue_free(&(*mapPtr)[existingIdx].value);
(*mapPtr)[existingIdx].value = valueToStore;
free(key); // The key is already stored in the map
} else {
shput(*mapPtr, key, RValue_makeIndependent(args[2]));
shput(*mapPtr, key, valueToStore);
}

return RValue_makeUndefined();
}

static RValue builtin_ds_map_add(VMContext* ctx, RValue* args, int32_t argCount) {
return dsMapAddCommon(ctx, args, argCount, false, 0);
}

static RValue builtin_ds_map_add_map(VMContext* ctx, RValue* args, int32_t argCount) {
return dsMapAddCommon(ctx, args, argCount, true, DS_TYPE_MAP);
}

static RValue builtin_ds_map_add_list(VMContext* ctx, RValue* args, int32_t argCount) {
return dsMapAddCommon(ctx, args, argCount, true, DS_TYPE_LIST);
}

static RValue builtin_ds_map_is_map(VMContext* ctx, RValue* args, int32_t argCount) {
if (2 > argCount) return RValue_makeBool(false);
Runner* runner = ctx->runner;
int32_t id = RValue_toInt32(args[0]);
DsMapEntry** mapPtr = dsMapGet(runner, id);
if (mapPtr == nullptr) return RValue_makeBool(false);

ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]);
if (0 > idx) return RValue_makeBool(false);

RValue val = (*mapPtr)[idx].value;
if (val.type == RVALUE_STRUCT && val.structInst != nullptr) {
RValue objType = VM_structGetVariableByVarName(ctx, val.structInst, "ObjType", -1);
if (objType.type != RVALUE_UNDEFINED) {
return RValue_makeBool(RValue_toInt32(objType) == DS_TYPE_MAP);
}
}
return RValue_makeBool(false);
}

static RValue builtin_ds_map_is_list(VMContext* ctx, RValue* args, int32_t argCount) {
if (2 > argCount) return RValue_makeBool(false);
Runner* runner = ctx->runner;
int32_t id = RValue_toInt32(args[0]);
DsMapEntry** mapPtr = dsMapGet(runner, id);
if (mapPtr == nullptr) return RValue_makeBool(false);

ptrdiff_t idx = getValueIndexInMap(mapPtr, args[1]);
if (0 > idx) return RValue_makeBool(false);

RValue val = (*mapPtr)[idx].value;
if (val.type == RVALUE_STRUCT && val.structInst != nullptr) {
RValue objType = VM_structGetVariableByVarName(ctx, val.structInst, "ObjType", -1);
if (objType.type != RVALUE_UNDEFINED) {
return RValue_makeBool(RValue_toInt32(objType) == DS_TYPE_LIST);
}
}
return RValue_makeBool(false);
}

static RValue builtin_ds_map_clear(VMContext* ctx, RValue* args, int32_t argCount) {
if (1 > argCount) return RValue_makeUndefined();
Runner* runner = ctx->runner;
Expand Down Expand Up @@ -4248,6 +4319,17 @@ static RValue builtin_ds_map_find_value(VMContext* ctx, RValue* args, int32_t ar

if (0 > idx) return RValue_makeUndefined();
RValue val = (*mapPtr)[idx].value;

if (val.type == RVALUE_STRUCT && val.structInst != nullptr) {
RValue objectVal = VM_structGetVariableByVarName(ctx, val.structInst, "Object", -1);
if (objectVal.type != RVALUE_UNDEFINED) {
RValue result = RValue_makeIndependent(objectVal);
RValue_free(&objectVal);
return result;
}
RValue_free(&objectVal);
}

if (val.type == RVALUE_STRING && val.string != nullptr) {
return RValue_makeOwnedString(safeStrdup(val.string));
}
Expand Down Expand Up @@ -9021,36 +9103,6 @@ static RValue builtin_buffer_async_group_end(MAYBE_UNUSED VMContext* ctx, MAYBE_
return RValue_makeReal((GMLReal) requestId);
}

// filename_change_ext(fname, newext): changes the extension of fname to newext
// (see GameMaker-HTML5 Function_File.js for reference)
static RValue builtin_filename_change_ext(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) {
if (2 > argCount) return RValue_makeUndefined();

char* fname = RValue_toString(args[0]);
char* newext = RValue_toString(args[1]); // includes the ., example: ".gmk"

char *last = strrchr(fname, '.');

if (last != nullptr && last != 0) {
long index = last - fname;
char* new_name = safeMalloc(index + strlen(newext) + 1);
memcpy(new_name, fname, (size_t) index);
memcpy(new_name + index, newext, (size_t) strlen(newext));
new_name[index + strlen(newext)] = '\0';
RValue result = RValue_makeOwnedString(new_name);

free(fname);
free(newext);

return result;
}

free(newext);

// If there isn't a dot, we return the original string as is
return RValue_makeOwnedString(fname);
}

static RValue builtin_buffer_base64_encode(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) {
Runner* runner = ctx->runner;
if (3 > argCount) return RValue_makeOwnedString(safeStrdup(""));
Expand Down Expand Up @@ -9222,6 +9274,60 @@ static RValue builtin_md5_file(MAYBE_UNUSED VMContext* ctx, RValue* args, MAYBE_
return RValue_makeOwnedString(convertToHexString(digest, 16));
}

// filename_change_ext(fname, newext): changes the extension of fname to newext
// (see GameMaker-HTML5 Function_File.js for reference)
static RValue builtin_filename_change_ext(MAYBE_UNUSED VMContext* ctx, MAYBE_UNUSED RValue* args, MAYBE_UNUSED int32_t argCount) {
if (2 > argCount) return RValue_makeUndefined();

char* fname = RValue_toString(args[0]);
char* newext = RValue_toString(args[1]); // includes the ., example: ".gmk"

char *last = strrchr(fname, '.');

if (last != nullptr && last != 0) {
long index = last - fname;
char* new_name = safeMalloc(index + strlen(newext) + 1);
memcpy(new_name, fname, (size_t) index);
memcpy(new_name + index, newext, (size_t) strlen(newext));
new_name[index + strlen(newext)] = '\0';
RValue result = RValue_makeOwnedString(new_name);

free(fname);
free(newext);

return result;
}

free(newext);

// If there isn't a dot, we return the original string as is
return RValue_makeOwnedString(fname);
}

// filename_name(fname): returns the name part of the indicated file, with the extension but without the path
// (see GameMaker-HTML5 Function_File.js for reference)
static RValue builtin_filename_name(MAYBE_UNUSED VMContext* ctx, RValue* args, int32_t argCount) {
if (1 > argCount) return RValue_makeOwnedString(safeStrdup(""));

char* fname = RValue_toString(args[0]);
if (fname == nullptr) return RValue_makeOwnedString(safeStrdup(""));

char* lastBackslash = strrchr(fname, '\\');
char* lastSlash = strrchr(fname, '/');
char* last = lastBackslash > lastSlash ? lastBackslash : lastSlash;

char* result;
if (last != nullptr) {
result = safeStrdup(last + 1);
} else {
result = fname;
fname = nullptr;
}

free(fname);
return RValue_makeOwnedString(result);
}

// buffer_get_surface(buffer, surface, offset) -> bool
// Reads RGBA8 pixels from the surface into the buffer at the given offset.
static RValue builtin_buffer_get_surface(VMContext* ctx, RValue* args, MAYBE_UNUSED int32_t argCount) {
Expand Down Expand Up @@ -14560,32 +14666,89 @@ static RValue builtin_json_encode(VMContext* ctx, RValue* args, int32_t argCount
return RValue_makeOwnedString(result);
}

// json_decode
// Recursively decode a JSON value into a GML value
static RValue jsonDecodeValue(VMContext* ctx, JsonValue* json) {
if (json == nullptr) return RValue_makeUndefined();

switch (json->type) {
case JSON_NULL:
return RValue_makeUndefined();
case JSON_BOOL:
return RValue_makeBool(json->boolValue);
case JSON_NUMBER:
return RValue_makeReal((GMLReal)json->numberValue);
case JSON_STRING:
return RValue_makeOwnedString(safeStrdup(json->stringValue ? json->stringValue : ""));
case JSON_ARRAY: {
// For arrays, create a ds_list (matches HTML5 - _json_decode_array)
int32_t listId = dsListCreate(ctx->runner);
DsList* list = dsListGet(ctx->runner, listId);
if (list != nullptr) {
int len = JsonReader_arrayLength(json);
for (int i = 0; i < len; i++) {
JsonValue* item = JsonReader_getArrayElement(json, i);
RValue val = jsonDecodeValue(ctx, item);
arrput(list->items, val);
}
}
return RValue_makeReal((GMLReal)listId);
}
case JSON_OBJECT: {
// For arrays, create a ds_map (matches HTML5 - _json_decode_object)
int32_t mapId = dsMapCreate(ctx->runner);
DsMapEntry** mapPtr = dsMapGet(ctx->runner, mapId);
if (mapPtr != nullptr) {
int len = JsonReader_objectLength(json);
for (int i = 0; i < len; i++) {
const char* key = JsonReader_getJsonKeyByIndex(json, i);
JsonValue* valJson = JsonReader_getJsonValueByIndex(json, i);
RValue val = jsonDecodeValue(ctx, valJson);

// Store in the map
char* keyCopy = safeStrdup(key);
RValue storedVal = RValue_makeIndependent(val);
RValue_free(&val);
shput(*mapPtr, keyCopy, storedVal);
}
}
return RValue_makeReal((GMLReal)mapId);
}
default:
return RValue_makeUndefined();
}
}

static RValue builtin_json_decode(VMContext* ctx, RValue* args, int32_t argCount) {
if (1 > argCount) {
fprintf(stderr, "[json_decode] Expected at least 1 argument\n");
return RValue_makeUndefined();
}

Runner* runner = ctx->runner;
int32_t mapIndex = dsMapCreate(runner);
DsMapEntry **mapPtr = dsMapGet(runner, mapIndex);
const char* content = args[0].string;

JsonValue* json = JsonReader_parse(content);
// While the docs say "An invalid DS Map handle (-1) is returned in case the JSON could not be decoded.", when looking at the GameMaker-HTML5 source code it actually wraps in a "default" block when it fails to be parsed
// While the docs say "An invalid ds_map handle (-1) is returned in case the JSON could not be decoded",
// when looking at the GameMaker-HTML5 source code it actually wraps in a "default" block when it fails to be parsed
if (json == nullptr) {
shput(*mapPtr, "default", RValue_makeIndependent(args[0]));
} else {
repeat(JsonReader_objectLength(json), i) {
const char *key = safeStrdup(JsonReader_getJsonKeyByIndex(json, i));
RValue val = RValue_makeOwnedString(safeStrdup(JsonReader_getString(JsonReader_getJsonValueByIndex(json, i))));
shput(*mapPtr, key, val);
int32_t mapIndex = dsMapCreate(runner);
DsMapEntry** mapPtr = dsMapGet(runner, mapIndex);
if (mapPtr != nullptr) {
shput(*mapPtr, safeStrdup("default"), RValue_makeIndependent(args[0]));
}
return RValue_makeReal((GMLReal)mapIndex);
}

// Recursively decode the JSON
RValue result = jsonDecodeValue(ctx, json);
JsonReader_free(json);

JsonReader_free(json);
// result should be a ds_map ID (from the top-level object)
if (result.type == RVALUE_REAL || result.type == RVALUE_INT32) {
return result;
}

return RValue_makeReal(mapIndex);
return RValue_makeReal((GMLReal)dsMapCreate(runner));
}

static RValue builtin_object_exists(VMContext* ctx, RValue* args, int32_t argCount) {
Expand Down Expand Up @@ -15557,6 +15720,10 @@ void VMBuiltins_registerAll(VMContext* ctx) {
VM_registerBuiltin(ctx, "ds_map_create", builtin_ds_map_create);
VM_registerBuiltin(ctx, "ds_map_delete", builtin_ds_map_delete);
VM_registerBuiltin(ctx, "ds_map_add", builtin_ds_map_add);
VM_registerBuiltin(ctx, "ds_map_add_map", builtin_ds_map_add_map);
VM_registerBuiltin(ctx, "ds_map_add_list", builtin_ds_map_add_list);
VM_registerBuiltin(ctx, "ds_map_is_map", builtin_ds_map_is_map);
VM_registerBuiltin(ctx, "ds_map_is_list", builtin_ds_map_is_list);
VM_registerBuiltin(ctx, "ds_map_clear", builtin_ds_map_clear);
VM_registerBuiltin(ctx, "ds_map_set", builtin_ds_map_set);
VM_registerBuiltin(ctx, "ds_map_set_pre", builtin_ds_map_set_pre);
Expand Down Expand Up @@ -15891,7 +16058,6 @@ void VMBuiltins_registerAll(VMContext* ctx) {
VM_registerBuiltin(ctx, "buffer_save_async", builtin_buffer_save_async);
VM_registerBuiltin(ctx, "buffer_async_group_begin", builtin_buffer_async_group_begin);
VM_registerBuiltin(ctx, "buffer_async_group_end", builtin_buffer_async_group_end);
VM_registerBuiltin(ctx, "filename_change_ext", builtin_filename_change_ext);
VM_registerBuiltin(ctx, "buffer_base64_encode", builtin_buffer_base64_encode);
VM_registerBuiltin(ctx, "buffer_base64_decode", builtin_buffer_base64_decode);
VM_registerBuiltin(ctx, "base64_encode", builtin_base64_encode);
Expand All @@ -15902,6 +16068,10 @@ void VMBuiltins_registerAll(VMContext* ctx) {
VM_registerBuiltin(ctx, "sha1_file", builtin_sha1_file);
VM_registerBuiltin(ctx, "md5_file", builtin_md5_file);

// Filename
VM_registerBuiltin(ctx, "filename_change_ext", builtin_filename_change_ext);
VM_registerBuiltin(ctx, "filename_name", builtin_filename_name);

// PSN
VM_registerBuiltin(ctx, "psn_init", builtin_psn_init);
VM_registerBuiltin(ctx, "psn_init_np_libs", builtin_psn_init_np_libs);
Expand Down
Loading