This is a FAQ + documentation for GMTogether by YellowAfterlife, a tool for playing local multiplayer GameMaker games over the internet.

The tool can be downloaded from the official discord server or itch.io page. Donations are accepted via itch.io.

An up-to-date version of this document can always be found online.

Click on sections to expand/collapse them.
Quick display controls: Categories · Sections · Everything ·

How does GMTogether work?

In contrast to conventional "cloud gaming" solutions, GMT is an engine-specific implementation of P2P lockstep networking - the kind of thing you see in higher-end emulators, fighting games, and some of the online multiplayer mods for games (including mine).

This offers a fairer split of lag between players, lower system/network requirements, and fairer handling of internet connection hickups.

For a slightly more in-depth comparison of netcode options, see this sheet.

As for the technical side, Microsoft Detours is used - if you've ever used software that augments Windows UI elements, this is pretty much the same, except for GameMaker runtime - so, for example, the game might poll Windows API functions for input as usual, but, instead of calling the system function directly, it would instead call GMT's function, which would return synchronized inputs that account for other player.

License

To keep it short and obvious,

By using the software you adhere to the following rules:

  1. Do not copy, rent, lend, lease, or otherwise distribute the software.
  2. Do not modify, reverse-engineer, decompile, disassemble or create derivative works of the software.
  3. Do not use the software for illegal activity.

These rules are subject of expansion/clarification.
Changes are assumed to apply retroactively.
An up-to-date version of license can be found at https://yal.cc/r/19/gmt/#li.


Installing GMTogether
General setup

Unzip the files somewhere - perhaps a separate directory, for convenience.

Per-game setup

If the game is a single executable, but you know that it's made with GM, try extracting it via 7-zip - it might be a self-extracting cabinet;

If you downloaded (from the sheet, etc.) any game-specific configuration files (GMT.ini / GMT-Gamepad.lua / custom cursors), unzip them into the game directory.

Using GMTogether

Drag and drop the game executable onto GMT-Launcher.exe (normal launch) or GMT-Windowed.exe (force windowed mode), like so:

If all is well, upon boot you will be met with a familiar-looking menu:

Here you can do a bunch of things:

Local mode

This opens a lobby menu without actually hosting a server on a port.

There are several uses for this:

  • You can verify that GMT doesn't break the game in some way without needing a second player/window.
  • You can use gamepad remapping feature to use the desired gamepad in games that do not natively support gamepad selection.
  • You can use/test KB+M -> gamepad mapping scripts.
Host

You enter a (UDP) port, and GMT hosts a server on that port.
Perhaps you've been here before, an absolute classic of P2P networking.

Also, don't forget to allow the game in your firewall if you have it enabled - since the game didn't do networking before, Windows most likely didn't prompt you to add a firewall whitelist entry for it.

Once connected, you can adjust delay setting (make sure it's high enough), remap gamepads if you need to, and start the actual game.

Join

You enter an IP and port, and we'll try to connect to it - by which I mean, repeatedly ping that endpoint with "hello" packets until they respond or you cancel.

Once connected, remap gamepads if you need to, and wait for host to start the game.

Delay (host-only, in-lobby)

The absolute classic option of lockstep networking, delay determines input latency but also how long we can wait for remote player's inputs before we have to pause the game and wait for them.

Swap Authority (host-only, in-lobby)

This peculiarly named option is actually very simple:
Let's suppose that you have a friend who cannot port forward, but they do have way more stuff unlocked in-game, or a large collection of downloaded content, or something else of great value for your sessions.

This option lets you swap places with them, instead making them the game host (as far as sync logic is concerned).

The new host can swap authority back if they need to.

Gamepad remapping

This small menu allows to rearrange gamepads as seen by the game.

Doing so allows you to pick non-overlapping gamepad slots (suppose both of you have a gamepad in slot 1, but the game should see them as 1 and 2), as well as being able to assign a simulated gamepad to one of the slots.

First you can view the current positioning of gamepads per slot, then you pick the source slot, then you pick the destination slot, and the gamepads are swapped between them.

Note that some games might only listen to gamepads in the first 4 slots.

Supported games

There are a few requirements for a game to work with GMT:

  • Must be made with GameMaker: Studio (>= ~1.4.1773) or GameMaker Studio 2.
  • Must have local multiplayer
    (unless you want to use it as "assist mode")
  • Must not use excessively exotic features (see below)

For your convenience, a sheet of tested games exists - it covers both whether the games work "out of box", and how you can make them work if they don't.

Limitations
Async functions

HTTP requests, networking, and select Steam API calls are not warranted to yield the same results between players, and neither to execute their events at the exact same frame. If a game relies on timing/results of this for gameplay-specific logic, it may result in desyncing.

Steam workshop

Somewhat tied to above, workshop file synchronization is a complicated topic, so for simplicity's sake GMT will just have the game assume that you do not have any subscribed items.

Native dialog boxes

At this time, dialogue functions (show_message, get_string, etc.) are not synchronized, therefore will show for both players and may cause state divergence if players answer differently.

Native extensions

If DLL functions are used for something that affects gameplay (suppose, reading savefiles), GMT has no means of warranting that the result will be the same between players. Usually a good way to guard against this is to manually ensure that your save files/etc. match up.

Use of DLLs for gameplay elements remains to be relatively uncommon in GM games, however.

Troubleshooting
Before we start

Before you start with this section, see if someone already made a configuration for the game in question via the supported games sheet.

(the menu doesn't show up)

Possible causes:

  • The game is not made with GameMaker.
    As in, so different from GM that it doesn't even initialize in the same way.
  • The game is a self-extracting archive.
    (try extracting it via 7-zip)
  • The game relaunches for one or other reason.
    GMT will auto-detect use of Steam API and prompt for steps required to workaround this, but other platform-specific launchers may require different approaches.
  • You have some software (perhaps an overzealous antivirus?) that disables DLL injection or other key components of Detours library.
"Failed to find the functions"

Possible causes:

  • The game isn't actually made in GameMaker at all
  • The game is made in a much older version of GameMaker
    (to the point that it doesn't look like a GM game to GMT)
"The game is too old"

This means that the game is made with GameMaker: Studio, but an old enough version that GMT is unable to use the normal approach to processing functions.

If the game is spotting a data.win file in its directory, you can try copying an executable from a slightly newer game to its directory and running that instead.

You can also try asking the game's author to try compiling the game in a slightly newer version of GameMaker: Studio if they have time.

Troubleshooting desyncs
Before we start

Same as with Troubleshooting section, it is possible that someone already figured out the issue and contributed a configuration file that fixes the issue(s) - see supported games sheet.

Intro

As with any tool that might seem to be working through means of pure magic, there's ought to be some sort of a catch - and I don't mean selling your soul or anything.

In case of GMT, some games might desynchronize. A desync is a game state divergence to the point where you aren't really playing the game together anymore, rather sending inputs to a remote player in an unlike situation.

GMT will auto-detect desyncs based on a set of common metrics (number of active game objects, RNG state, active game level, etc.) and notify both of you of the fact.

GMT will also save a screenshot and a summary of game state for each of the players, which can then be combined for inspection (either by eye in case of screenshots, or via "text diff" tools for states) to figure out what might've went wrong.

Game resolution and mouse position

This is perhaps the biggest curse of GMT: GameMaker games do a reasonable thing and store mouse cursor coordinates as a fraction-less number relative to window's top-left corner.

As result, if your windows are of different sizes (or, if you are running in fullscreen, your screens are), mouse position will not match up between players.

This being a common issue, GMT supports a number of workarounds:

  • If one player's screen is larger than the other's, GMT will attempt to fix this automatically by adjusting reported window size and offset (thus acting as if the smaller screen is centered in the middle of the larger screen).
    This should take care of oddities in most games.
  • Many games support running in windowed mode and adjusting window size, so you can try that first.
  • GMT-Windowed.bat will run the game in a window and prevent it from going full-screen through common means.
  • Adjusting window_set_size in tweaks section of GMT.ini allows to override game window size and prevent the game from changing it normally.
  • Adjusting display_get_size in tweaks section of GMT.ini allows to override screen resolution reported to the game.
Per-Steam-user-ID save files

Some games use Steam User IDs for determining save directory name, which you can usually tell by navigating there and checking if there's a long-number-name directory (e.g. 76561198025072099).

To force the game to report the same Steam ID for both players, you can use steam_get_user_steam_id in tweaks section of GMT.ini.

General FAQ
Can you support Mac/Linux?

In theory yes, but in practice this requires a complete rewrite for both of them.

This is due to runtime differences, OS differences, CPU instruction differences, and differences in how hooking/instrumentation is done per-OS.

In other words, unless we suddenly invent infinite time or something, you might have to use WINE/Crossover/Proton to play the win32 versions of the games.

Can you support non-GameMaker games?

While some of the work I've done for GMT is non-GameMaker-specific, supporting arbitrary games is an impressive task to undertake, as pretty much any goal can be achieved in multiple ways, and there is no warranty of any given game using any given approach for doing so.

Maintaining such a tool is likely also a full-time job, so, yeah.

Can you support >2 computers?

While you may have any number of players per computer, GMT only supports having 2 computers in a game session.

The decision was made to keep the tool's (already strange-looking) netcode bearable to work with - things are so much easier when you just have to make sure that the other player is good to go rather than making sure that all players are good to go, and also making sure that if someone drops out, everyone are up to date with they were doing, and also account for possibility of some players losing connections to each other, and...

So, yeah, complexity. Maybe someday.

Can you make a universal mod system?

In theory yes, but it would be less cool (or far more costly) than you think.

So, as is evident from the Lua API for gamepads, having Lua code that interacts with GML instances is a possibility, albeit with noticeable development costs in terms of making GML functions available to Lua and not easily crashing the game.

However, "simply" being able to change variables and call functions isn't nearly enough - to be able to actually introduce new content to the game you most often need to be able to add new objects, scripts, or even segments to existing scripts (for example, Nuclear Throne handles all weapon firing code in one massive script with a 125-case switch-block).

Doing so in a way that works with both VM and YYC games and a wide variety of GameMaker versions (which had fairly drastic internal structure changes in past) is a task of great complexity and development/maintenance cost.

There are also ethical considerations with it suddenly being possible to inject arbitrary code into arbitrary games, especially for games with online interactions, leaderboards, or active speedrunning communities.

How is this free?

Great question!

Firstly, it is nice to release things for free when you can - moreso when they can change how people can play (some of the) games as such.

But also it is impossible to warrant that the tool would work for every single game, and it would surely be a shame to buy a tool only to find that it doesn't work for what you were going to use it for. And other option? Having to write fancy DRM for a trial? Surely is own kind of fun, but also I have better things to do.

I'd like to help but I have no money
  • Tell your friends!
    (it helps more than you might think)
    (if you have no friends, tell people that you talk to often?)
  • Test some games!
    (helps iron out bugs/figure out tweaks for oddities)
  • Contribute configuration files for games!
    (if you are tech-savvy enough to do so)
  • Stream/record videos of playing games via the tool!
    (if that's your thing)
Developer FAQ
My game doesn't work correctly with GMT, can you help?

I may be available for consultation or hire for debugging issues;

See limitations for things that are known to not work.

Given a budget, previously unsupported features can be covered instead of tweaking the game (to not use them).

Overall, you can get in touch.

Does this mean that I don't have to do online multiplayer now?

Yes and no, but mostly no.

For players, it means that your game not having online multiplayer isn't the end of the world anymore (as in, people can still play it alright if they want), but you'd still want an actual integration for it to be a selling point.

Or, at least, to the next point,

Can I use GMT in my game?

If you are content with how GMT works and would love that as an official option in your game, you can get in touch about licensing - custom features/UI are possible, along with actually having it built-in rather than as a piece of external software.

In general, GMT working well enough for a game is a sign that deterministic netcode is an option, which is also something that you can hire me for (or someone else, for that matter). Or just picking what is the best option if you can tell that there's community interest in this.

Otherwise you can make an announcement/prominently feature it as a well-working option to play your game online.

Advanced topics
Game-specific configuration files

GMT allows to write game-specific configuration files.

These offer a wide variety of tweaks to get the games running correctly, a few features to reduce perceived latency (such as native cursors), a few "quality of life" features (such as mouselock), and some miscellaneous options.

The included GMT-Example.ini has examples of all supported options alongside with explanations of their functions. To make use of it, copy it to the game directory, rename it from GMT-Example to GMT, and edit it to your liking.

Options are loaded on game startup.
Game-affecting options will automatically apply to both players, so only the host player will usually need to edit the file.

Virtual gamepads
Premise

Some games are best played with keyboard & mouse.
But - what if both of you would prefer to use KB+M?
While this is a solved problem, the amount of games that actually support using multiple mice and keyboards is fairly low... to say the least.

If you create a file called GMT-Gamepad.lua, GMT will load it up and offer it as a virtual gamepad option on remap menu menu and your virtual gamepad script will be able to define which keyboard/mouse/gamepad controls will correspond to which virtual gamepad controls.

Note: if you are the game's developer, you can also use the player-specific input API instead.

You can use a very minimalist subset of GM-specific functions here.

Keyboard functions

Custom:

keyboard_ignore(key, ignore = true)

Ignores (or un-ignores) a key. This prevents it from being applied to the game, which is good if the remote player would like to use this same button for keyboard+mouse controls. Use vk_anykey to apply to all keys at once.

Mouse functions

Custom:

mouse_ignore_button(button, ignore = true)

Ignores (or un-ignores) a mouse button, akin to keyboard_ignore.
Use mb_any to apply to all buttons at once.

mouse_ignore_wheel(ignore = true)

Ignores (or un-ignores) mouse wheel events.

mouse_ignore_move(ignore = true)

Ignores (or un-ignores) mouse movements.

Gamepad functions

Custom:

gamepad_virtual

A global variable storing the index (0..11) of gamepad that was mapped to your gamepad script. You'll usually want to set outputs for this gamepad, although you may also output to several gamepads if you need to.

gamepad_set_connected(index, is_connected)

Changes whether the gamepad is connected.
This is fine to call every frame - the game will only receive async events upon status change.

gamepad_button_set(index, button, value)

Changes status of a given button.
value can be true, false, 1, or 0.
button should be one of gp_ constants.
Pressed/released states will be generated automatically from this.

gamepad_axis_set(index, axis, value)

Similar to above, but for gamepad axes.
value should be a number in -1..+1 range.

Instance functions
<number>.field

GMT runtime comes with a small metatable trick so that field access on numeric GML instance IDs maps to variable_instance_get.

The runtime will also auto-expose object names.

To find out object and variable names, you can enable dumpKey in tweaks section of GMT.ini (see GMT-Example.ini) and press that at points of interest to save a dump.gml file into the game's save directory.

with(object)

Lua obviously doesn't have a with-loop, but you might need to loop over instances to figure out which player instance is yours.

So you get this function, which returns an iterator for instance IDs.

local dx = 0
local dy = 0
for p in with(obj_player) do
	if (p.index == 0) then
		dx = mouse_get_x() - p.x
		dy = mouse_get_y() - p.y
		break
	end
end
-- normalize the offset into a length-1 vector:
local dl = math.sqrt(dx * dx + dy * dy)
if (dl ~= 0) then
	dx = dx / dl
	dy = dy / dl
end
gamepad_axis_set(i, gp_axisrh, dx)
gamepad_axis_set(i, gp_axisrv, dy)
Window functions
window_set_cursor(cursor)

Unlike the built-in function, it can be also called with a custom cursor path as a string.

Note that the later does not have any caching, so maybe don't call it every frame.

Misc functions
print(...)

For lack of stdout in GM games, Lua's print function will instead print to the log file.

trace(...)

Akin to print, but will instead show text in top-left corner of the game for one frame.

Callbacks
(entrypoint)

Your Lua script is first loaded up on game start - after the menu is shown and before any action begins.

update()

This function is called once per frame and is where you do what you must.

Example:

keyboard_ignore(vk_anykey)
mouse_ignore_button(mb_any)
mouse_ignore_move()
function update()
	local i = gamepad_virtual
	gamepad_set_connected(i, true)
	--
	local lx, ly = 0, 0
	if (keyboard_check(string.byte("D"))) then lx = lx + 1 end
	if (keyboard_check(string.byte("A"))) then lx = lx - 1 end
	if (keyboard_check(string.byte("S"))) then ly = ly + 1 end
	if (keyboard_check(string.byte("W"))) then ly = ly - 1 end
	gamepad_axis_set(i, gp_axislh, lx)
	gamepad_axis_set(i, gp_axislv, ly)
	--
	gamepad_button_set(i, gp_start, keyboard_check(vk_enter))
	gamepad_button_set(i, gp_face1, keyboard_check(vk_space))
end
Interfacing with GMT in your GML code
os_get_info()

GMT adds a few additional fields to os_get_info,

  • gmt:is_server: Whether you are the server (1) or guest (0).
    When testing locally, this is also 1.
  • gmt:net_count: The number of computers in the session.
    Currently this is going to be 1 or 2.
  • gmt:net_index: The index of the local player.
    This has 0...net_count-1 range.
  • gmt:net_delay: Input delay, in frames.
    You may use this in custom logic to make the game more forgiving if players have high input latency.
  • gmt:is_replay: Indicates that GMT is currently displaying a replay.
    You can use this to adjust UI accordingly (e.g. show inputs).
  • gmt:was_replay: Indicates that the current session is a replay, or started from a replay. This can be used to allow "spliced" scores (resuming mid-replay to correct for a mistake) while sorting them into a separate leaderboard.
  • gmt:is_tas: Indicates that playback features are enabled.
    Similarly, you can use this to auto-categorize submissions.

Example (making different views visible for host/guest):

var map = os_get_info();
if (ds_map_exists(map, "gmt:is_server")) {
    if (map[?"gmt:net_index"] == 0) {
        view_visible[0] = true;
        view_visible[1] = false;
    } else {
        view_visible[0] = false;
        view_visible[1] = true;
    }
}
Player-specific file operations

By default, save directory operations will use the host's (authority) files.

However, you can also read files of a specific player using special syntax,

@<0-based player index>:/<relative file path>

So, @0:/save.ini would read the host's save.ini, while @1:/save.ini would read the guest's save.ini.

This can be used for combining save files of multiple players on startup and saving specific unlocks achieved in an online session.

Example:

// ... on game start
var map = os_get_info();
global.gmt = ds_map_exists(map, "gmt:is_server");
global.next_player_index = 0;
instance_create_depth(x, y, 0, obj_player);

if (global.gmt && map[?"gmt:net_count"] > 1) {
    // if there's a second player, spawn them in too
    global.next_player_index = 1;
    instance_create_depth(x, y, 0, obj_player);
}

and then, in obj_player's Create event:

index = global.next_player_index;
if (global.gmt) {
    ini_open("@" + string(index) + ":/save.ini");
} else ini_open("save.ini");
name = ini_read_string("player", "name", "");
// ... read other variables too
ini_close();

This would read each player's name from their respective local save.ini file.

Player-specific keyboard/mouse polling

Although writing Lua gamepad handlers is an option, if you are the game's developer, there is also a more convenient one.

If you run

debug_event("gmt:multidevice");

while GMT is enabled, multi-device input mode will be enabled. This enables a few non-standard uses of GameMaker functions:

  • keyboard_check*(key + 256 * one_based_index):
    Polls a key from specific player.
    So keyboard_check(vk_left) would poll Left key from any player (as before), keyboard_check(vk_left + 256) would poll Left from P1 (host/authority), and keyboard_check(vk_left + 512) would poll Left from P2 (guest).
    Similarly works for keyboard_check_pressed and keyboard_check_released.
  • device_mouse_*(one_based_index, ...): Polls mouse data from a specific player.
    So, device_mouse_x(0) would poll from any player (as before), device_mouse_x(1) would poll from P1 (host/authority), and device_mouse_x(2) would poll from P2 (guest). Works for all device_mouse_* functions.
Credits

Software by YellowAfterlife.

Uses Lua for scripting elements.

Uses Microsoft Detours for function rerouting.

Uses a modified version of a function by Semantic Designs for x86 instruction parsing.

Uses a function signature search trick by Saturnyoshi for faster startup in select GM versions.