diff --git a/LICENSE b/LICENSE index 79c2dfc..10246a9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Jaanus Jaggo (Perfoon) +Copyright (c) 2021 Jaanus Jaggo (Perfoon) & Dreadpon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b628045..3189145 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,52 @@ Multirun allows starting multiple game instances at once. The main purpose of this feature is to speed up multiplayer game development. One game instance can be configured to host the game and others to join. +![showcase](https://i.postimg.cc/PqwFqP7N/showcase.gif) + +Tested on Godot v3.5 + +## Installation + +This plugin is installed the same way as other Godot plugins. + +Download the code by clicking green `Code` button and then `Download ZIP`. Unpack it anywhere you like. + +Copy the folder `addons/dreadpon.spatial_gardener/` to `res://addons/` in your Godot project and enable it from `Project -> Project Settings -> Plugins`. + +**NOTE:** this plugin relies on an autoload singleton `res://addons/multirun/instance_setup.gd` under the name of `MultirunInstanceSetup` to run. +If your windows aren't positioning themselves correctly, check if `MultirunInstanceSetup` is properly loaded. + +![autoload-setup](screenshots/autoload-setup.jpg) + ## How to use -1. Add the plugin to your project and enable it. -2. Configure the plugin in Project Settings. The settings are located under *Debug → Multirun*. -3. Run the script by clicking the multirun button on the top right corner of Godot editor, or press F4 on keyboard. +Launching game instances can be done via buttons in the top right corner of Godot editor. +- 1 - open `user://` directory +- 2 - run all instances (or *re*launch if already launched) +- 3 - run a specific instance (will honor it's window positioning and custom `user://` directory) +- 4 - stop all running instances + +![launch-instances](screenshots/launch-instances.jpg) -Extra: next to the multirun button there is also a new folder button that opens the `"user://"` path when clicked. +This plugin also supports personal subfolders for each instance ran. They are meant to be used instead of a regular `user://` path. -![Screenshot](screenshots/MultirunPreview.png) +![subdirectories](screenshots/subdirectories.jpg) + +A personal path can be accessed via singleton `MultirunInstanceSetup.instance_user_dir` or `MultirunInstanceSetup.get_user_path()`. When launched standalone, this path will refer to `user://` as usual and only change when you are, indeed, "multirunning" your game. ## Settings -Under the Project Settings there is a new category *Debug → Multirun* with the following parameters: -* **Window Distance** - distance in pixels between different windows. It offsets the windows so that they don't appear on top of each other. -* **Number of Windows** - the total number of windows it opens. -* **Add Custom Args** - when checked, it will add the user defined command line arguments to the opened game instances. -* **First Window Args** - custom command line arguments that will be applied to the first window. To add multiple arguments, separate them with a space. -* **Other Window Args** - custom command line arguments that will be applied to all other windows. To add multiple arguments, separate them with a space. +You can configure the following settings in the `Project -> Project Settings -> Multirun`: +- `Number Of Windows` to run +- `Distance Between Windows` if you need extra space betwenen them. **NOTE:** there's some space between them by default: Windows OS includes "active zones" (for manually changing window size) when calculating window bounds +- `Individual Instance Args` - console arguments to pass to each instance as dictionary of instace indices as keys and string of arguments as values. **NOTE:** to pass the same args to ALL instances use the key of `-1` +- Key `Shortcuts` for actions defined in paragraph 1 -![Screenshot](screenshots/MultirunSettings.png) +![settings](screenshots/settings.jpg) ## Additional Information Finding problems in the code, open a ticket on [GitHub](https://github.com/perfoon/Multirun/issues). +**NOTE:** it might take some time to merge v.2.0.0 to main, in which case Dreadpon should be tagged for their resolution. + diff --git a/addons/multirun/Multirun.gd b/addons/multirun/Multirun.gd deleted file mode 100644 index 5d41fa0..0000000 --- a/addons/multirun/Multirun.gd +++ /dev/null @@ -1,94 +0,0 @@ -tool -extends EditorPlugin - -var panel1 -var panel2 -var pids = [] - -func _enter_tree(): - var editor_node = get_tree().get_root().get_child(0) - var gui_base = editor_node.get_gui_base() - var icon_transition = gui_base.get_icon("TransitionSync", "EditorIcons") #ToolConnect - var icon_transition_auto = gui_base.get_icon("TransitionSyncAuto", "EditorIcons") - var icon_load = gui_base.get_icon("Load", "EditorIcons") - - panel2 = _add_tooblar_button("_loaddir_pressed", icon_load, icon_load) - panel1 = _add_tooblar_button("_multirun_pressed", icon_transition, icon_transition_auto) - - _add_setting("debug/multirun/number_of_windows", TYPE_INT, 2) - _add_setting("debug/multirun/window_distance", TYPE_INT, 1270) - _add_setting("debug/multirun/add_custom_args", TYPE_BOOL, true) - _add_setting("debug/multirun/first_window_args", TYPE_STRING, "listen") - _add_setting("debug/multirun/other_window_args", TYPE_STRING, "join") - -func _multirun_pressed(): - var window_count : int = ProjectSettings.get_setting("debug/multirun/number_of_windows") - var window_dist : int = ProjectSettings.get_setting("debug/multirun/window_distance") - var add_custom_args : bool = ProjectSettings.get_setting("debug/multirun/add_custom_args") - var first_args : String = ProjectSettings.get_setting("debug/multirun/first_window_args") - var other_args : String = ProjectSettings.get_setting("debug/multirun/other_window_args") - var commands = ["--position", "50,10"] - if first_args && add_custom_args: - for arg in first_args.split(" "): - commands.push_front(arg) - - var main_run_args = ProjectSettings.get_setting("editor/main_run_args") - if main_run_args != first_args: - ProjectSettings.set_setting("editor/main_run_args", first_args) - var interface = get_editor_interface() - interface.play_main_scene() - if main_run_args != first_args: - ProjectSettings.set_setting("editor/main_run_args", main_run_args) - - kill_pids() - for i in range(window_count-1): - commands = ["--position", str(50 + (i+1) * window_dist) + ",10"] - if other_args && add_custom_args: - for arg in other_args.split(" "): - commands.push_front(arg) - pids.append(OS.execute(OS.get_executable_path(), commands, false)) - -func _loaddir_pressed(): - OS.shell_open(OS.get_user_data_dir()) - -func _exit_tree(): - _remove_panels() - kill_pids() - -func kill_pids(): - for pid in pids: - OS.kill(pid) - pids = [] - -func _remove_panels(): - if panel1: - remove_control_from_container(CONTAINER_TOOLBAR, panel1) - panel1.free() - if panel2: - remove_control_from_container(CONTAINER_TOOLBAR, panel2) - panel2.free() - -func _unhandled_input(event): - if event is InputEventKey: - if event.pressed and event.scancode == KEY_F4: - _multirun_pressed() - -func _add_tooblar_button(action:String, icon_normal, icon_pressed): - var panel = PanelContainer.new() - var b = TextureButton.new(); - b.texture_normal = icon_normal - b.texture_pressed = icon_pressed - b.connect("pressed", self, action) - panel.add_child(b) - add_control_to_container(CONTAINER_TOOLBAR, panel) - return panel - -func _add_setting(name:String, type, value): - if ProjectSettings.has_setting(name): - return - ProjectSettings.set(name, value) - var property_info = { - "name": name, - "type": type - } - ProjectSettings.add_property_info(property_info) diff --git a/addons/multirun/instance_setup.gd b/addons/multirun/instance_setup.gd new file mode 100644 index 0000000..113600c --- /dev/null +++ b/addons/multirun/instance_setup.gd @@ -0,0 +1,81 @@ +extends Node + + +#------------------------------------------------------------------------------- +# A per-instance setup for multirun instances +# Transforms instance windows according to passed cmdline arguments +# And exposes an instance-specific 'user://' subfolder +# And can be used to manually write things like settings and save files +#------------------------------------------------------------------------------- + + +# When running multiple game instances you might want to have separate 'user://' folders for each +# So you can store settings and data unique to these instances +# When launched with an argument '--user_subfolder=path' 'path' will be appended to the current 'user://' dir +# Otherwise will refer to 'user://' dir +# Settings that are to be separate for each instance should use THIS path instead of 'user://' +var instance_user_dir: String = '' + + + + +func _ready(): + setup_instance(parse_cmdline_args()) + + +# Parse cmdline arguments into a Dictionaey +# Stripping all special symbols +func parse_cmdline_args() -> Dictionary: + var arguments = {} + for argument in OS.get_cmdline_args(): + if argument.find("=") > -1: + var key_value = argument.split("=") + arguments[key_value[0].lstrip("--")] = key_value[1] + else: + # Options without an argument might be present in the dictionary, + # With the value set to an empty string. + arguments[argument.lstrip("--")] = "" + return arguments + + +# Set this instance up according to passed arguments +# Mostly related to window transform +func setup_instance(arguments: Dictionary): + instance_user_dir = 'user://' + var decorations_size = OS.get_real_window_size() - OS.window_size + + for arg_name in arguments: + var arg_val = arguments[arg_name] + match arg_name: + + 'window_pos_x': + OS.window_position = Vector2(int(arg_val), OS.window_position.y) + + 'window_pos_y': + OS.window_position = Vector2(OS.window_position.x, int(arg_val)) + + 'window_size_x': + OS.window_size = Vector2(int(arg_val), OS.window_size.y) + + 'window_size_y': + OS.window_size = Vector2(OS.window_size.x, int(arg_val)) + + 'window_title': + OS.set_window_title(arguments.window_title) + + 'user_subfolder': + instance_user_dir += arg_val + instance_user_dir.replace('\\', '/') + if !instance_user_dir.ends_with('/'): + instance_user_dir += '/' + Directory.new().make_dir_recursive(instance_user_dir) + + OS.window_size -= decorations_size + + +# Get full path to dir/file with the instance dir path +# If we were to replicate 'user://dir/file.cfg' +# We would pass just 'dir/file.cfg' +# And receive 'user://instance_user_dir/dir/file.cfg' +func get_user_path(relative_path: String) -> String: + return instance_user_dir + relative_path diff --git a/addons/multirun/plugin.cfg b/addons/multirun/plugin.cfg index 5112dff..183c14e 100644 --- a/addons/multirun/plugin.cfg +++ b/addons/multirun/plugin.cfg @@ -1,7 +1,7 @@ [plugin] name="Multirun" -description="Multirun allows to start multiple game instances at once." -author="Jaanus Jaggo" -version="1.1.0" -script="Multirun.gd" \ No newline at end of file +description="Plugin to run multiple game instances at once, layed out in a neat grid." +author="Jaanus Jaggo & Dreadpon" +version="2.0.0" +script="plugin.gd" \ No newline at end of file diff --git a/addons/multirun/plugin.gd b/addons/multirun/plugin.gd new file mode 100644 index 0000000..370583b --- /dev/null +++ b/addons/multirun/plugin.gd @@ -0,0 +1,427 @@ +tool +extends EditorPlugin + + +#------------------------------------------------------------------------------- +# Due to plugin's size, everything is handled here +# Besides per-instance setup +#------------------------------------------------------------------------------- + + +const PTH_NUMBER_OF_WINDOWS: String = 'multirun/settings/number_of_windows' +const PTH_DISTANCE_BETWEEN_WINDOWS: String = 'multirun/settings/distance_between_window' +const PTH_INDIVIDUAL_INSTANCE_ARGS: String = 'multirun/settings/individual_instance_args' +const PTH_USER_DIR_SHORTCUT: String = 'multirun/shortcuts/user_dir' +const PTH_RUN_SHORTCUT: String = 'multirun/shortcuts/run' +const PTH_RUN_INSTANCE_SHORTCUT: String = 'multirun/shortcuts/run_instance' +const PTH_STOP_SHORTCUT: String = 'multirun/shortcuts/stop' + + +var icons: Dictionary = {} +var button_user_dir: ToolButton = null +var button_run: ToolButton = null +var button_run_instance: OptionButton = null +var button_stop: ToolButton = null + +var instance_pids: Dictionary = {} +var are_instances_running: bool = false setget _set_are_instances_running +var refresh_timer: Timer = Timer.new() +var refresh_wait_time: float = 0.5 + + + + +#------------------------------------------------------------------------------- +# Lifecycle +#------------------------------------------------------------------------------- + + +func _ready(): + add_autoload_singleton('MultirunInstanceSetup', 'res://addons/multirun/instance_setup.gd') + _setup_refresh_timer() + _update_button_icons() + + +func _enter_tree(): + _cache_icons() + _add_settings() + _add_buttons() + ProjectSettings.connect('project_settings_changed', self, '_on_project_settings_changed') + + +func _exit_tree(): + _remove_buttons() + kill_instances() + ProjectSettings.disconnect('project_settings_changed', self, '_on_project_settings_changed') + + +# We want to update run button icon when no instances are running +# But doing so requires constant polling, so we due it at small intervals +func _setup_refresh_timer(): + refresh_timer.wait_time = refresh_wait_time + refresh_timer.autostart = true + refresh_timer.one_shot = false + refresh_timer.connect('timeout', self, '_refresh_state') + add_child(refresh_timer) + + +# Refresh our plugin state periodically +func _refresh_state(): + if !_is_any_instance_running() && are_instances_running: + _stop_pressed() + + + + +#------------------------------------------------------------------------------- +# UI management +#------------------------------------------------------------------------------- + + +# Cache icons so we won't have to query for them again +func _cache_icons(): + var editor_node = get_tree().get_root().get_child(0) + var gui_base = editor_node.get_gui_base() + icons.load = gui_base.get_icon("Load", "EditorIcons") + icons.transition = gui_base.get_icon("TransitionSync", "EditorIcons") + icons.stop = gui_base.get_icon("Stop", "EditorIcons") + icons.rotate_left = gui_base.get_icon("RotateLeft", "EditorIcons") + icons.transition_end= gui_base.get_icon("TransitionEnd", "EditorIcons") + + +func _add_buttons(): + button_user_dir = _add_toolbar_button( + "_user_dir_pressed", icons.load, + ProjectSettings.get_setting(PTH_USER_DIR_SHORTCUT), + 'Open "user://" directory.') + + button_run = _add_toolbar_button( + "_run_pressed", icons.transition, + ProjectSettings.get_setting(PTH_RUN_SHORTCUT), + 'Run multiple instances of the main scene.') + + button_run_instance = _add_run_instance_button( + "_run_instance_pressed", icons.transition_end, + ProjectSettings.get_setting(PTH_RUN_INSTANCE_SHORTCUT), + 'Run a specific (numbered) instance of the main scene.') + + button_stop = _add_toolbar_button( + "_stop_pressed", icons.stop, + ProjectSettings.get_setting(PTH_STOP_SHORTCUT), + 'Stop all running instances of the main scene.') + + +func _remove_buttons(): + if button_run: + remove_control_from_container(CONTAINER_TOOLBAR, button_run) + button_run.queue_free() + if button_run_instance: + remove_control_from_container(CONTAINER_TOOLBAR, button_run_instance) + button_run_instance.queue_free() + if button_user_dir: + remove_control_from_container(CONTAINER_TOOLBAR, button_user_dir) + button_user_dir.queue_free() + if button_stop: + remove_control_from_container(CONTAINER_TOOLBAR, button_stop) + button_stop.queue_free() + + +func _add_run_instance_button(method_name: String, icon, shortcut: Dictionary = {}, tooltip: String = '') -> OptionButton: + var button = OptionButton.new(); + add_control_to_container(CONTAINER_TOOLBAR, button) + + button.icon = icon + if shortcut: + button.shortcut = _shortcut_from_dict(shortcut) + button.shortcut_in_tooltip = true + button.hint_tooltip = tooltip + button.connect('item_selected', self, '_run_instance_instance_pressed') + + return button + + +func _add_toolbar_button(method_name: String, icon, shortcut: Dictionary = {}, tooltip: String = '') -> ToolButton: + var button = ToolButton.new(); + add_control_to_container(CONTAINER_TOOLBAR, button) + + button.icon = icon + if shortcut: + button.shortcut = _shortcut_from_dict(shortcut) + button.shortcut_in_tooltip = true + button.hint_tooltip = tooltip + button.connect("pressed", self, method_name) + + return button + + +func _update_button_icons(): + if are_instances_running: + button_run.icon = icons.rotate_left + button_stop.disabled = false + else: + button_run.icon = icons.transition + button_stop.disabled = true + + + + +#------------------------------------------------------------------------------- +# UI events/signals +#------------------------------------------------------------------------------- + + +func _user_dir_pressed(): + open_user_data_dir() + + +func _run_pressed(): + run_instances() + + +func _run_instance_instance_pressed(idx: int): + button_run_instance.select(-1) + button_run_instance.icon = icons.transition_end + run_instances([idx]) + + +func _stop_pressed(): + kill_instances() + + + + +#------------------------------------------------------------------------------- +# Project settings management +#------------------------------------------------------------------------------- + + +func _add_settings(): + _add_setting(PTH_NUMBER_OF_WINDOWS, TYPE_INT, 4) + _add_setting(PTH_DISTANCE_BETWEEN_WINDOWS, TYPE_INT, 0) + # A Dictionary of + # { 'window_idx': args:String } + # A key of '-1' would mean arguments applied to ALL windows + # Except those with an individual override in the same dictionary + # NOTE: Overrides do not combine arguments, they replaces the whole argument string + _add_setting(PTH_INDIVIDUAL_INSTANCE_ARGS, TYPE_DICTIONARY, {}) + + var user_dir_shortcut := mk_dummy_shortcut_dict() + user_dir_shortcut.scancode = KEY_F9 + user_dir_shortcut.control = true + _add_setting(PTH_USER_DIR_SHORTCUT, TYPE_DICTIONARY, user_dir_shortcut) + + var run_shortcut := mk_dummy_shortcut_dict() + run_shortcut.scancode = KEY_F5 + run_shortcut.control = true + _add_setting(PTH_RUN_SHORTCUT, TYPE_DICTIONARY, run_shortcut) + + var run_instance_shortcut := mk_dummy_shortcut_dict() + run_instance_shortcut.scancode = KEY_F7 + run_instance_shortcut.control = true + _add_setting(PTH_RUN_INSTANCE_SHORTCUT, TYPE_DICTIONARY, run_instance_shortcut) + + var stop_shortcut := mk_dummy_shortcut_dict() + stop_shortcut.scancode = KEY_F8 + stop_shortcut.control = true + _add_setting(PTH_STOP_SHORTCUT, TYPE_DICTIONARY, stop_shortcut) + + +func _on_project_settings_changed(): + var window_count: int = ProjectSettings.get_setting(PTH_NUMBER_OF_WINDOWS) + button_run_instance.clear() + for idx in range(0, window_count): + button_run_instance.add_item('Instance %d' % [idx + 1]) + button_run_instance.select(-1) + button_run_instance.icon = icons.transition_end + + +# Shorthand for adding a setting +func _add_setting(name:String, type, value): + if ProjectSettings.has_setting(name): + return + ProjectSettings.set(name, value) + var property_info = { + "name": name, + "type": type + } + ProjectSettings.add_property_info(property_info) + + + + +#------------------------------------------------------------------------------- +# Instance management +#------------------------------------------------------------------------------- + + +# instance_list of -1 == run all instances +func run_instances(instance_list: Array = [-1]): + kill_instances(instance_list) + + var window_count: int = ProjectSettings.get_setting(PTH_NUMBER_OF_WINDOWS) + # Later on we will mix in distance between adjacent windows + # I don't know why someone might need it, but here it is + var window_dist: int = ProjectSettings.get_setting(PTH_DISTANCE_BETWEEN_WINDOWS) + var individual_instance_args: Dictionary = ProjectSettings.get_setting(PTH_INDIVIDUAL_INSTANCE_ARGS) + var main_run_args: Array = PoolStringArray(ProjectSettings.get_setting("editor/main_run_args").split(' ')) + + # We make some assumptions on how windows are laid out + # But this guess is fairly good for up to 12 instances + # More than 8 seems excess anyways + var screen_size := get_available_screen_size() + var columns := int(ceil(window_count / 3.0)) + var rows := int(ceil(float(window_count) / columns)) + screen_size -= Vector2(window_dist * (columns - 1), window_dist * (rows - 1)) + + if instance_list.has(-1): + instance_list = [] + for i in range(0, window_count): + instance_list.append(i) + + for i in instance_list: + var size := screen_size / Vector2(columns, rows) + var pos := Vector2() + pos.x = (i % columns) * (screen_size.x / columns) + (i % columns) * window_dist + pos.y = (i / columns) * (screen_size.y / rows) + (i / columns) * window_dist + var window_title = 'Instance-1 Main' + if i != 0: + window_title = 'Instance-%d' % [i + 1] + + var instance_args := [ + '--window_pos_x=%d' % [int(pos.x)], + '--window_pos_y=%d' % [int(pos.y)], + '--window_size_x=%d' % [int(size.x)], + '--window_size_y=%d' % [int(size.y)], + '--window_title="%s"' % [window_title], + '--user_subfolder="multirun_inst_%d"' % [i] + ] + + # We assume the main scene and our additional instances have an equal status (i.e. "A game being launched") + # Thus all arguments passed to the main scene on regular 'Run' (from ProjectSettings) + # Should be passed to our instances as well + instance_args.append_array(main_run_args) + + # Append per-instance arguments if present + if individual_instance_args.has(i): + instance_args.append_array(individual_instance_args[i].split(' ')) + # Append defualt arguments for all instances if present + elif individual_instance_args.has(-1): + instance_args.append_array(individual_instance_args[-1].split(' ')) + + # If running first instance, run it through intended/native means + if i == 0: + run_main_instance(instance_args, main_run_args) + # If not, run executable with arguments + else: + var path = OS.get_executable_path() + match OS.get_name(): + 'Windows': + var windows_args = ['/k', '"%s" %s' % [path, PoolStringArray(instance_args).join(' ')]] + instance_pids[i] = OS.execute('cmd', windows_args, false, [], false, true) + _: + instance_pids[i] = OS.execute(path, instance_args, false, [], false, true) + + _set_are_instances_running(true) + + +# We want to feed our arguments to the main scene as well +# But to preserve the project setting, we need to set it to previous value +# After we're done +func run_main_instance(instance_args: Array, main_run_args: Array): + var interface = get_editor_interface() + ProjectSettings.set_setting("editor/main_run_args", PoolStringArray(instance_args).join(' ')) + interface.play_main_scene() + ProjectSettings.set_setting("editor/main_run_args", PoolStringArray(main_run_args).join(' ')) + + +func kill_instances(instance_list: Array = [-1]): + var window_count: int = ProjectSettings.get_setting(PTH_NUMBER_OF_WINDOWS) + if instance_list.has(-1): + instance_list = [] + for i in range(0, window_count): + instance_list.append(i) + + for i in instance_list: + if i == 0: + _kill_main_instance() + continue + if !instance_pids.has(i): continue + + var pid = instance_pids[i] + match OS.get_name(): + 'Windows': + OS.execute('taskkill', ['/T', '/PID', str(pid)]) + _: + OS.kill(pid) + instance_pids.erase(i) + + _set_are_instances_running(false) + + +func _kill_main_instance(): + var interface = get_editor_interface() + interface.stop_playing_scene() + + +func _set_are_instances_running(val): + are_instances_running = val + _update_button_icons() + + +func _is_any_instance_running(): + for pid in instance_pids.values(): + if OS.is_process_running(pid): + return true + + var interface := get_editor_interface() + if interface.is_playing_scene(): + return true + + return false + + + + +#------------------------------------------------------------------------------- +# Misc +#------------------------------------------------------------------------------- + + +# Opening a user dir can be relevant if we need to test settings/save files +# Being written for each instance ran +func open_user_data_dir(): + OS.shell_open(OS.get_user_data_dir()) + + +# Get the screen size that is left after excluding screen-occupying elements +# Like Windows taskbar +func get_available_screen_size() -> Vector2: + var prev_maximized = OS.window_maximized + OS.window_maximized = true + var screen_size = OS.get_real_window_size() + OS.window_maximized = prev_maximized + return screen_size + + + +func _shortcut_from_dict(dict: Dictionary) -> ShortCut: + var shortcut = ShortCut.new() + shortcut.shortcut = InputEventKey.new() + shortcut.shortcut.scancode = dict.scancode + shortcut.shortcut.alt = dict.alt + shortcut.shortcut.shift = dict.shift + shortcut.shortcut.meta = dict.meta + shortcut.shortcut.command = dict.command + shortcut.shortcut.control = dict.control + return shortcut + + +func mk_dummy_shortcut_dict() -> Dictionary: + return { + 'scancode': -1, + 'alt': false, + 'shift': false, + 'control': false, + 'meta': false, + 'command': false, + } diff --git a/screenshots/MultirunPreview.png b/screenshots/MultirunPreview.png deleted file mode 100644 index c7755a7..0000000 Binary files a/screenshots/MultirunPreview.png and /dev/null differ diff --git a/screenshots/MultirunSettings.png b/screenshots/MultirunSettings.png deleted file mode 100644 index f83634b..0000000 Binary files a/screenshots/MultirunSettings.png and /dev/null differ diff --git a/screenshots/autoload-setup.jpg b/screenshots/autoload-setup.jpg new file mode 100644 index 0000000..f633111 Binary files /dev/null and b/screenshots/autoload-setup.jpg differ diff --git a/screenshots/launch-instances.jpg b/screenshots/launch-instances.jpg new file mode 100644 index 0000000..6853f8f Binary files /dev/null and b/screenshots/launch-instances.jpg differ diff --git a/screenshots/settings.jpg b/screenshots/settings.jpg new file mode 100644 index 0000000..6799db3 Binary files /dev/null and b/screenshots/settings.jpg differ diff --git a/screenshots/subdirectories.jpg b/screenshots/subdirectories.jpg new file mode 100644 index 0000000..f3e078f Binary files /dev/null and b/screenshots/subdirectories.jpg differ