Skip to content

ini_write_string silently writes empty string for non-string values #415

Description

@qum1nzhennan

Summary

ini_write_string() silently persists an empty string whenever the third argument is not already an RVALUE_STRING. Native GameMaker converts any value to a string, so GML like ini_write_string("S", "K", 123) must persist K="123", but Butterscotch writes K="".

Environment

  • Butterscotch built from main (relevant code: src/vm_builtins.c)
  • Android frontend
  • Game: DELTARUNE (GMS 2.3 / WAD 17 bytecode) with the gm3dr/DeltaruneChinese community patch installed

Root cause

src/vm_builtins.c, builtin_ini_write_string:

const char* value = (args[2].type == RVALUE_STRING ? args[2].string : "");

Any non-string third argument is silently discarded and replaced with "". The adjacent builtin_ini_write_real already does the right thing:

char* valueStr = RValue_toString(args[2]);

Minimal repro

ini_open("test.ini");
ini_write_string("TEST", "VALUE", 123);
ini_close();

Resulting file on disk (wrong):

[TEST]
VALUE=""

Native GameMaker writes VALUE="123".

Real-world failure this caused

Deltarune's Chinese localization patch implements the in-game name translation toggle like this:

function scr_change_language() {
    global.names = (global.names + 1) % 3;              // number: 0/1/2
    ossafe_ini_open("true_config.ini");
    ini_write_string("L10N_ZH", "NAMES", global.names); // numeric value
    ossafe_ini_close();
    ossafe_savedata_save();
    scr_84_init_localization(); // immediately reads ini_read_real("L10N_ZH", "NAMES", 0)
}

Instrumentation confirmed the whole chain fires on Android: virtual Z key down -> keyboard_check(90)=true -> input_pressed[4]=1 -> button1_p()=1 -> scr_change_language() runs, and true_config.ini is rewritten at that exact moment - but with:

[L10N_ZH]
NAMES=""

scr_84_init_localization() then reads "" back as real 0, so the toggle instantly reverts and the option appears to do nothing.

Suggested fix

static RValue builtin_ini_write_string(VMContext* ctx, RValue* args, int32_t argCount) {
    Runner* runner = ctx->runner;
    if (3 > argCount || runner->currentIni == nullptr) return RValue_makeUndefined();

    const char* section = (args[0].type == RVALUE_STRING ? args[0].string : "");
    const char* key = (args[1].type == RVALUE_STRING ? args[1].string : "");
    char* value = RValue_toString(args[2]);

    Ini_setString(runner->currentIni, section, key, value);
    runner->currentIniDirty = true;
    free(value);
    return RValue_makeUndefined();
}

Happy to open a PR with this fix if that helps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions