Skip to content

Instantly share code, notes, and snippets.

@nkrapivin
Last active August 23, 2023 19:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nkrapivin/9237d82357bd90c7195a23e40c1166db to your computer and use it in GitHub Desktop.
Save nkrapivin/9237d82357bd90c7195a23e40c1166db to your computer and use it in GitHub Desktop.
Adding LiveSplit support into your GameMaker game.

Adding LiveSplit support into your GameMaker game

This guide is intended for developers who want to assist speedrunners with creating auto-splitters but don't know how to do it.

You don't actually need to use any external DLLs, all you need is one huge buffer that will contain all the information needed for speedrunners.

Why should I bother?

Great question!

While speedrunners can figure the needed pointer paths by themselves, since GameMaker stores all variables internally in a "robbinhood hashmap" even a tiny change to the game's code may screw up all the pointer paths.

By following this guide you'll be able to tell speedrunners the values of certain variables (that you control) through a chunk of memory that will never change between game versions, GameMaker versions and even between operating systems.

Code Example:

Put this somewhere at your initialization code (has to be done only once):

ls_buffer = -1;

if (use_livesplit) {
    ls_buffer = buffer_create(2048, buffer_fixed, 1); // < allocate a large enough fixed buffer to prevent reallocations
    buffer_fill(ls_buffer, 0, buffer_u8, 0, buffer_get_size(ls_buffer)); // < force the memory to be ACTUALLY allocated by GM
    buffer_seek(ls_buffer, buffer_seek_start, 0); // < just in case seek to the beginning
    // You must come up with your own MAGIC so that your buffer can easily be searched for on the heap.
    // It must be at least 16 bytes long and be unique enough so that it won't be confused with the game's code or text.
    // do NOT store the magic as a string! use an array! because otherwise there is a chance that ASL's scanner
    // might find the constant string instead of the buffer's contents. an array of numbers is stored in a different way.
    
    // this code assumes you'll put 32 numbers in there (from range 0 to 255, buffer_u8)
    var MAGIC = [ /* your magic numbers go here .... */ ];
    MAGICsize = array_length(MAGIC);
    for (var Mi = 0, Ml = MAGICsize; Mi < Ml; ++Mi) {
        buffer_write(ls_buffer, buffer_u8, MAGIC[@ Mi]);
    }
    buffer_seek(ls_buffer, buffer_seek_start, 0); // < seek back again after writing the magic
    
    // put data after MAGICsize:
    // here you can write data that never changes while the game is running
    buffer_poke(ls_buffer, MAGICsize, buffer_text, GM_version); // < [32..64)
    // GM_version is a built-in engine constant that takes the version from the Project Settings
    // you may not use that and you can write something else instead
    buffer_poke(ls_buffer, MAGICsize + 32, buffer_text, os_get_config()); // [64..96)
    // this returns the build config, again, most small games have only one
    
    // be sure that you write at least some kind of version so that speedrunners
    // will be able to differentiate between versions of your game
    // in case you make breaking changes to the logic/or to the buffer layout
    
    // just in case write this to the debug log (accessible by using -output o.log launchparam)
    show_debug_message("BUFFER ADDRESS = " + string(buffer_get_address(ls_buffer)));
    show_debug_message("END!"); // < this is needed so the previous line gets flushed just in case
}

use_livesplit is the flag that determines whether to allocate the buffer or not.

You can read it from save data, or if you've already frozen your save data format and cannot change it, you can use game's launch parameters instead by checking parameter_string and parameter_count:

use_livesplit = false;
for (var p_i = 0, p_c = parameter_count(); p_i <= p_c; ++p_i) {
	var p_s = string_lower(parameter_string(p_i));
	switch (p_s) {
		case "--livesplit":
		case "-livesplit": {
			use_livesplit = true;
			break;
		}
	}
}

Now you must update this buffer every frame of the game, you can do so in the Step event at the appropriate moment:

// all GameMaker number variables are stored in either buffer_f64 (real) or buffer_s64 (int64) format, usually it is the former.
// both buffer_f64 and buffer_s64 are 8 bytes in size
// all GameMaker strings can be written using buffer_string (writes the nullbyte) or buffer_text (does not do that), I suggest you use the former.
// the string format is "UTF-8 without BOM", always, GameMaker does not support "wide strings" or any other type of string.
if (ls_buffer >= 0) {
    // config ends at 95th byte, continue at offset 96
    // put some plot related variables, for example a plot counter, dialogue counter, whatever...
    // make sure to not break the layout and keep it consistent
    buffer_poke(ls_buffer, 96, buffer_f64, some_global_object.some_plot_counter); // [96..104)
    buffer_poke(ls_buffer, 104, buffer_string, room_get_name(room)); // [104..168)
    // add extra data at offset 168 and beyond...
    // if you're writing a string make sure to reserve some fixed space for it, usually 64 bytes is enough.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment