diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 55fd98b7be..6b79a2f267 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -636,6 +636,7 @@
+
diff --git a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
new file mode 100644
index 0000000000..a59f5d5fbd
--- /dev/null
+++ b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
@@ -0,0 +1,360 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2019 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using OpenRA.Network;
+using OpenRA.Widgets;
+
+namespace OpenRA.Mods.Common.Widgets.Logic
+{
+ public class GameSaveBrowserLogic : ChromeLogic
+ {
+ readonly Widget panel;
+ readonly ScrollPanelWidget gameList;
+ readonly TextFieldWidget saveTextField;
+ readonly List games = new List();
+ readonly Action onStart;
+ readonly Action onExit;
+ readonly ModData modData;
+ readonly bool isSavePanel;
+ readonly string baseSavePath;
+
+ readonly string defaultSaveFilename;
+ string selectedSave;
+
+ [ObjectCreator.UseCtor]
+ public GameSaveBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart, bool isSavePanel, World world)
+ {
+ panel = widget;
+
+ this.modData = modData;
+ this.onStart = onStart;
+ this.onExit = onExit;
+ this.isSavePanel = isSavePanel;
+ Game.BeforeGameStart += OnGameStart;
+
+ panel.Get("CANCEL_BUTTON").OnClick = () =>
+ {
+ Ui.CloseWindow();
+ onExit();
+ };
+
+ gameList = panel.Get("GAME_LIST");
+ var gameTemplate = panel.Get("GAME_TEMPLATE");
+ var newTemplate = panel.Get("NEW_TEMPLATE");
+
+ var mod = modData.Manifest;
+ baseSavePath = Platform.ResolvePath(Platform.SupportDirPrefix, "Saves", mod.Id, mod.Metadata.Version);
+
+ // Avoid filename conflicts when creating new saves
+ if (isSavePanel)
+ {
+ panel.Get("SAVE_TITLE").IsVisible = () => true;
+
+ defaultSaveFilename = world.Map.Title;
+ var filenameAttempt = 0;
+ while (true)
+ {
+ if (!File.Exists(Path.Combine(baseSavePath, defaultSaveFilename + ".orasav")))
+ break;
+
+ defaultSaveFilename = world.Map.Title + " ({0})".F(++filenameAttempt);
+ }
+
+ var saveButton = panel.Get("SAVE_BUTTON");
+ saveButton.OnClick = () => { Save(world); };
+ saveButton.IsVisible = () => true;
+
+ var saveWidgets = panel.Get("SAVE_WIDGETS");
+ saveTextField = saveWidgets.Get("SAVE_TEXTFIELD");
+ gameList.Bounds.Height -= saveWidgets.Bounds.Height;
+ saveWidgets.IsVisible = () => true;
+ }
+ else
+ {
+ panel.Get("LOAD_TITLE").IsVisible = () => true;
+ var loadButton = panel.Get("LOAD_BUTTON");
+ loadButton.IsVisible = () => true;
+ loadButton.IsDisabled = () => selectedSave == null;
+ loadButton.OnClick = () => { Load(); };
+ }
+
+ if (Directory.Exists(baseSavePath))
+ LoadGames(gameTemplate, newTemplate);
+
+ var renameButton = panel.Get("RENAME_BUTTON");
+ renameButton.IsDisabled = () => selectedSave == null;
+ renameButton.OnClick = () =>
+ {
+ var initialName = Path.GetFileNameWithoutExtension(selectedSave);
+ var invalidChars = Path.GetInvalidFileNameChars();
+
+ ConfirmationDialogs.TextInputPrompt(
+ "Rename Save",
+ "Enter a new file name:",
+ initialName,
+ onAccept: newName => Rename(initialName, newName),
+ onCancel: null,
+ acceptText: "Rename",
+ cancelText: null,
+ inputValidator: newName =>
+ {
+ if (newName == initialName)
+ return false;
+
+ if (string.IsNullOrWhiteSpace(newName))
+ return false;
+
+ if (newName.IndexOfAny(invalidChars) >= 0)
+ return false;
+
+ if (File.Exists(Path.Combine(baseSavePath, newName)))
+ return false;
+
+ return true;
+ });
+ };
+
+ var deleteButton = panel.Get("DELETE_BUTTON");
+ deleteButton.IsDisabled = () => selectedSave == null;
+ deleteButton.OnClick = () =>
+ {
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Delete selected game save?",
+ text: "Delete '{0}'?".F(Path.GetFileNameWithoutExtension(selectedSave)),
+ onConfirm: () =>
+ {
+ Delete(selectedSave);
+
+ if (!games.Any() && !isSavePanel)
+ {
+ Ui.CloseWindow();
+ onExit();
+ }
+ else
+ SelectFirstVisible();
+ },
+ confirmText: "Delete",
+ onCancel: () => { });
+ };
+
+ var deleteAllButton = panel.Get("DELETE_ALL_BUTTON");
+ deleteAllButton.IsDisabled = () => !games.Any();
+ deleteAllButton.OnClick = () =>
+ {
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Delete all game saves?",
+ text: "Delete {0} game saves?".F(games.Count),
+ onConfirm: () =>
+ {
+ foreach (var s in games.ToList())
+ Delete(s);
+
+ Ui.CloseWindow();
+ onExit();
+ },
+ confirmText: "Delete All",
+ onCancel: () => { });
+ };
+
+ SelectFirstVisible();
+ }
+
+ void LoadGames(ScrollItemWidget gameTemplate, ScrollItemWidget newTemplate)
+ {
+ gameList.RemoveChildren();
+ if (isSavePanel)
+ {
+ var item = ScrollItemWidget.Setup(newTemplate,
+ () => selectedSave == null,
+ () => Select(null),
+ () => { });
+ gameList.AddChild(item);
+ }
+
+ var savePaths = Directory.GetFiles(baseSavePath, "*.orasav", SearchOption.AllDirectories)
+ .OrderByDescending(p => File.GetLastWriteTime(p))
+ .ToList();
+
+ foreach (var savePath in savePaths)
+ {
+ games.Add(savePath);
+
+ // Create the item manually so the click handlers can refer to itself
+ // This simplifies the rename handling (only needs to update ItemKey)
+ var item = gameTemplate.Clone() as ScrollItemWidget;
+ item.ItemKey = savePath;
+ item.IsVisible = () => true;
+ item.IsSelected = () => selectedSave == item.ItemKey;
+ item.OnClick = () => Select(item.ItemKey);
+ item.OnDoubleClick = Load;
+
+ var title = Path.GetFileNameWithoutExtension(savePath);
+ item.Get("TITLE").GetText = () => title;
+
+ var date = File.GetLastWriteTime(savePath).ToString("yyyy-MM-dd HH:mm:ss");
+ item.Get("DATE").GetText = () => date;
+
+ gameList.AddChild(item);
+ }
+ }
+
+ void Rename(string oldName, string newName)
+ {
+ try
+ {
+ var oldPath = Path.Combine(baseSavePath, oldName + ".orasav");
+ var newPath = Path.Combine(baseSavePath, newName + ".orasav");
+ File.Move(oldPath, newPath);
+
+ games[games.IndexOf(oldPath)] = newPath;
+ foreach (var c in gameList.Children)
+ {
+ var item = c as ScrollItemWidget;
+ if (item == null || item.ItemKey != oldPath)
+ continue;
+
+ item.ItemKey = newPath;
+ item.Get("TITLE").GetText = () => newName;
+ }
+
+ if (selectedSave == oldPath)
+ selectedSave = newPath;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ Log.Write("debug", ex.ToString());
+ }
+ }
+
+ void Delete(string savePath)
+ {
+ try
+ {
+ File.Delete(savePath);
+ }
+ catch (Exception ex)
+ {
+ Game.Debug("Failed to delete save file '{0}'. See the logs for details.", savePath);
+ Log.Write("debug", ex.ToString());
+ return;
+ }
+
+ if (savePath == selectedSave)
+ Select(null);
+
+ var item = gameList.Children
+ .Select(c => c as ScrollItemWidget)
+ .FirstOrDefault(c => c.ItemKey == savePath);
+
+ gameList.RemoveChild(item);
+ games.Remove(savePath);
+ }
+
+ void SelectFirstVisible()
+ {
+ Select(isSavePanel ? null : games.FirstOrDefault());
+ }
+
+ void Select(string savePath)
+ {
+ selectedSave = savePath;
+ if (isSavePanel)
+ saveTextField.Text = savePath == null ? defaultSaveFilename :
+ Path.GetFileNameWithoutExtension(savePath);
+ }
+
+ void Load()
+ {
+ if (selectedSave == null)
+ return;
+
+ // Parse the save to find the map UID
+ var save = new GameSave(selectedSave);
+ var map = modData.MapCache[save.GlobalSettings.Map];
+ if (map.Status != MapStatus.Available)
+ return;
+
+ var orders = new List()
+ {
+ new Order("LoadGameSave", null, false)
+ {
+ IsImmediate = true,
+ TargetString = Path.GetFileName(selectedSave)
+ },
+ Order.Command("state {0}".F(Session.ClientState.Ready))
+ };
+
+ Game.CreateAndStartLocalServer(map.Uid, orders);
+ }
+
+ void Save(World world)
+ {
+ var filename = saveTextField.Text + ".orasav";
+ var testPath = Platform.ResolvePath(
+ Platform.SupportDirPrefix,
+ "Saves",
+ modData.Manifest.Id,
+ modData.Manifest.Metadata.Version,
+ filename);
+
+ Action inner = () =>
+ {
+ world.RequestGameSave(filename);
+ Ui.CloseWindow();
+ onExit();
+ };
+
+ if (selectedSave != null || File.Exists(testPath))
+ {
+ ConfirmationDialogs.ButtonPrompt(
+ title: "Overwrite save game?",
+ text: "Overwrite {0}?".F(saveTextField.Text),
+ onConfirm: inner,
+ confirmText: "Overwrite",
+ onCancel: () => { });
+ }
+ else
+ inner();
+ }
+
+ void OnGameStart()
+ {
+ Ui.CloseWindow();
+ onStart();
+ }
+
+ bool disposed;
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && !disposed)
+ {
+ disposed = true;
+ Game.BeforeGameStart -= OnGameStart;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public static bool IsLoadPanelEnabled(Manifest mod)
+ {
+ var baseSavePath = Platform.ResolvePath(Platform.SupportDirPrefix, "Saves", mod.Id, mod.Metadata.Version);
+ if (!Directory.Exists(baseSavePath))
+ return false;
+
+ return Directory.GetFiles(baseSavePath, "*.orasav", SearchOption.AllDirectories).Any();
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs
index b2c48d50e1..87f6980d82 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameMenuLogic.cs
@@ -49,6 +49,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
{ "ABORT_MISSION", CreateAbortMissionButton },
{ "SURRENDER", CreateSurrenderButton },
+ { "LOAD_GAME", CreateLoadGameButton },
+ { "SAVE_GAME", CreateSaveGameButton },
{ "MUSIC", CreateMusicButton },
{ "SETTINGS", CreateSettingsButton },
{ "RESUME", CreateResumeButton },
@@ -251,6 +253,46 @@ namespace OpenRA.Mods.Common.Widgets.Logic
};
}
+ void CreateLoadGameButton()
+ {
+ if (world.Type != WorldType.Regular || !world.LobbyInfo.GlobalSettings.GameSavesEnabled || world.IsReplay)
+ return;
+
+ var button = AddButton("LOAD_GAME", "Load Game");
+ button.IsDisabled = () => leaving || !GameSaveBrowserLogic.IsLoadPanelEnabled(modData.Manifest);
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => hideMenu = false },
+ { "onStart", CloseMenu },
+ { "isSavePanel", false },
+ { "world", null }
+ });
+ };
+ }
+
+ void CreateSaveGameButton()
+ {
+ if (world.Type != WorldType.Regular || !world.LobbyInfo.GlobalSettings.GameSavesEnabled || world.IsReplay)
+ return;
+
+ var button = AddButton("SAVE_GAME", "Save Game");
+ button.IsDisabled = () => hasError || leaving || !world.Players.Any(p => p.Playable && p.WinState == WinState.Undefined);
+ button.OnClick = () =>
+ {
+ hideMenu = true;
+ Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => hideMenu = false },
+ { "onStart", () => { } },
+ { "isSavePanel", true },
+ { "world", world }
+ });
+ };
+ }
+
void CreateMusicButton()
{
var button = AddButton("MUSIC", "Music");
diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs
index 75b87e268b..187f30124f 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs
@@ -28,7 +28,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
protected enum MenuType { Main, Singleplayer, Extras, MapEditor, SystemInfoPrompt, None }
- protected enum MenuPanel { None, Missions, Skirmish, Multiplayer, MapEditor, Replays }
+ protected enum MenuPanel { None, Missions, Skirmish, Multiplayer, MapEditor, Replays, GameSaves }
protected MenuType menuType = MenuType.Main;
readonly Widget rootMenu;
@@ -127,6 +127,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
skirmishButton.OnClick = StartSkirmishGame;
skirmishButton.Disabled = !hasMaps;
+ var loadButton = singleplayerMenu.Get("LOAD_BUTTON");
+ loadButton.IsDisabled = () => !GameSaveBrowserLogic.IsLoadPanelEnabled(modData.Manifest);
+ loadButton.OnClick = OpenGameSaveBrowserPanel;
+
singleplayerMenu.Get("BACK_BUTTON").OnClick = () => SwitchMenu(MenuType.Main);
// Extras menu
@@ -500,6 +504,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic
});
}
+ void OpenGameSaveBrowserPanel()
+ {
+ SwitchMenu(MenuType.None);
+ Ui.OpenWindow("GAMESAVE_BROWSER_PANEL", new WidgetArgs
+ {
+ { "onExit", () => SwitchMenu(MenuType.Singleplayer) },
+ { "onStart", () => { RemoveShellmapUI(); lastGameState = MenuPanel.GameSaves; } },
+ { "isSavePanel", false },
+ { "world", null }
+ });
+ }
+
protected override void Dispose(bool disposing)
{
if (disposing)
@@ -535,6 +551,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
case MenuPanel.MapEditor:
SwitchMenu(MenuType.MapEditor);
break;
+
+ case MenuPanel.GameSaves:
+ SwitchMenu(MenuType.Singleplayer);
+ break;
}
lastGameState = MenuPanel.None;
diff --git a/mods/cnc/chrome/gamesave-browser.yaml b/mods/cnc/chrome/gamesave-browser.yaml
new file mode 100644
index 0000000000..011f420462
--- /dev/null
+++ b/mods/cnc/chrome/gamesave-browser.yaml
@@ -0,0 +1,115 @@
+Container@GAMESAVE_BROWSER_PANEL:
+ Logic: GameSaveBrowserLogic
+ X: (WINDOW_RIGHT - WIDTH) / 2
+ Y: (WINDOW_BOTTOM - HEIGHT) / 2
+ Width: 600
+ Height: 400
+ Children:
+ Label@LOAD_TITLE:
+ Width: PARENT_RIGHT
+ Y: 0 - 25
+ Font: BigBold
+ Contrast: true
+ Align: Center
+ Text: Load game
+ Visible: False
+ Label@SAVE_TITLE:
+ Width: PARENT_RIGHT
+ Y: 0 - 25
+ Font: BigBold
+ Contrast: true
+ Align: Center
+ Text: Save game
+ Visible: False
+ Background@bg:
+ Width: PARENT_RIGHT
+ Height: PARENT_BOTTOM
+ Background: panel-black
+ Children:
+ ScrollPanel@GAME_LIST:
+ X: 10
+ Y: 10
+ Width: PARENT_RIGHT - 20
+ Height: PARENT_BOTTOM - 20
+ Children:
+ ScrollItem@NEW_TEMPLATE:
+ Width: PARENT_RIGHT - 27
+ Height: 25
+ X: 2
+ Visible: false
+ Children:
+ Label@TITLE:
+ Width: PARENT_RIGHT
+ Height: PARENT_BOTTOM
+ Align: Center
+ Text: [CREATE NEW FILE]
+ ScrollItem@GAME_TEMPLATE:
+ Width: PARENT_RIGHT - 27
+ Height: 25
+ X: 2
+ Visible: false
+ Children:
+ Label@TITLE:
+ X: 10
+ Width: PARENT_RIGHT - 200 - 10
+ Height: 25
+ Label@DATE:
+ X: PARENT_RIGHT - WIDTH - 10
+ Width: 200
+ Height: 25
+ Align: Right
+ Container@SAVE_WIDGETS:
+ X: 10
+ Y: PARENT_BOTTOM - 35
+ Width: PARENT_RIGHT - 20
+ Height: 35
+ Visible: False
+ Children:
+ TextField@SAVE_TEXTFIELD:
+ Width: PARENT_RIGHT
+ Height: 25
+ Type: Filename
+ Button@CANCEL_BUTTON:
+ Key: escape
+ X: 0
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Back
+ Button@DELETE_ALL_BUTTON:
+ X: PARENT_RIGHT - 330 - WIDTH
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Delete All
+ Button@DELETE_BUTTON:
+ X: PARENT_RIGHT - 220 - WIDTH
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Delete
+ Key: Delete
+ Button@RENAME_BUTTON:
+ X: PARENT_RIGHT - 110 - WIDTH
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Rename
+ Key: F2
+ Button@LOAD_BUTTON:
+ Key: return
+ X: PARENT_RIGHT - WIDTH
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Load
+ Visible: False
+ Button@SAVE_BUTTON:
+ Key: return
+ X: PARENT_RIGHT - WIDTH
+ Y: PARENT_BOTTOM - 1
+ Width: 100
+ Height: 35
+ Text: Save
+ Visible: False
+ TooltipContainer@TOOLTIP_CONTAINER:
diff --git a/mods/cnc/chrome/mainmenu.yaml b/mods/cnc/chrome/mainmenu.yaml
index 24f6f18049..f40a9a6940 100644
--- a/mods/cnc/chrome/mainmenu.yaml
+++ b/mods/cnc/chrome/mainmenu.yaml
@@ -119,6 +119,12 @@ Container@MENU_BACKGROUND:
Width: 140
Height: 35
Text: Missions
+ Button@LOAD_BUTTON:
+ X: 300
+ Y: 0
+ Width: 140
+ Height: 35
+ Text: Load
Button@BACK_BUTTON:
Key: escape
X: 450
diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml
index 3fa975c9f6..1103495f5f 100644
--- a/mods/cnc/mod.yaml
+++ b/mods/cnc/mod.yaml
@@ -107,6 +107,7 @@ ChromeLayout:
cnc|chrome/color-picker.yaml
cnc|chrome/mapchooser.yaml
cnc|chrome/replaybrowser.yaml
+ cnc|chrome/gamesave-browser.yaml
cnc|chrome/gamesave-loading.yaml
cnc|chrome/ingame.yaml
cnc|chrome/ingame-chat.yaml
diff --git a/mods/common/chrome/gamesave-browser.yaml b/mods/common/chrome/gamesave-browser.yaml
new file mode 100644
index 0000000000..0e29ed3f75
--- /dev/null
+++ b/mods/common/chrome/gamesave-browser.yaml
@@ -0,0 +1,116 @@
+Background@GAMESAVE_BROWSER_PANEL:
+ Logic: GameSaveBrowserLogic
+ X: (WINDOW_RIGHT - WIDTH) / 2
+ Y: (WINDOW_BOTTOM - HEIGHT) / 2
+ Width: 600
+ Height: 400
+ Children:
+ Label@LOAD_TITLE:
+ Width: PARENT_RIGHT
+ Y: 20
+ Height: 25
+ Font: Bold
+ Align: Center
+ Text: Load game
+ Visible: False
+ Label@SAVE_TITLE:
+ Width: PARENT_RIGHT
+ Y: 20
+ Height: 25
+ Font: Bold
+ Align: Center
+ Text: Save game
+ Visible: False
+ ScrollPanel@GAME_LIST:
+ X: 20
+ Y: 50
+ Width: PARENT_RIGHT - 40
+ Height: PARENT_BOTTOM - 102
+ Children:
+ ScrollItem@NEW_TEMPLATE:
+ Width: PARENT_RIGHT - 27
+ Height: 25
+ X: 2
+ Visible: false
+ Children:
+ Label@TITLE:
+ Width: PARENT_RIGHT
+ Height: PARENT_BOTTOM
+ Align: Center
+ Text: [CREATE NEW FILE]
+ ScrollItem@GAME_TEMPLATE:
+ Width: PARENT_RIGHT - 27
+ Height: 25
+ X: 2
+ Visible: false
+ Children:
+ Label@TITLE:
+ X: 10
+ Width: PARENT_RIGHT - 200 - 10
+ Height: 25
+ Label@DATE:
+ X: PARENT_RIGHT - WIDTH - 10
+ Width: 200
+ Height: 25
+ Align: Right
+ Container@SAVE_WIDGETS:
+ X: 20
+ Y: PARENT_BOTTOM - 77
+ Width: PARENT_RIGHT - 40
+ Height: 32
+ Visible: False
+ Children:
+ TextField@SAVE_TEXTFIELD:
+ Width: PARENT_RIGHT
+ Height: 25
+ Type: Filename
+ Button@CANCEL_BUTTON:
+ Key: escape
+ X: 20
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Back
+ Font: Bold
+ Button@DELETE_ALL_BUTTON:
+ X: PARENT_RIGHT - 350 - WIDTH
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Delete All
+ Font: Bold
+ Button@DELETE_BUTTON:
+ X: PARENT_RIGHT - 240 - WIDTH
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Delete
+ Font: Bold
+ Key: Delete
+ Button@RENAME_BUTTON:
+ X: PARENT_RIGHT - 130 - WIDTH
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Rename
+ Font: Bold
+ Key: F2
+ Button@LOAD_BUTTON:
+ Key: return
+ X: PARENT_RIGHT - WIDTH - 20
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Load
+ Font: Bold
+ Visible: False
+ Button@SAVE_BUTTON:
+ Key: return
+ X: PARENT_RIGHT - WIDTH - 20
+ Y: PARENT_BOTTOM - 45
+ Width: 100
+ Height: 25
+ Text: Save
+ Font: Bold
+ Visible: False
+ TooltipContainer@TOOLTIP_CONTAINER:
diff --git a/mods/common/chrome/mainmenu.yaml b/mods/common/chrome/mainmenu.yaml
index b17af26690..6af9bf8c61 100644
--- a/mods/common/chrome/mainmenu.yaml
+++ b/mods/common/chrome/mainmenu.yaml
@@ -114,6 +114,13 @@ Container@MAINMENU:
Height: 30
Text: Missions
Font: Bold
+ Button@LOAD_BUTTON:
+ X: PARENT_RIGHT / 2 - WIDTH / 2
+ Y: 140
+ Width: 140
+ Height: 30
+ Text: Load
+ Font: Bold
Button@BACK_BUTTON:
X: PARENT_RIGHT / 2 - WIDTH / 2
Key: escape
diff --git a/mods/d2k/chrome/mainmenu.yaml b/mods/d2k/chrome/mainmenu.yaml
index dd517591fe..37008147ae 100644
--- a/mods/d2k/chrome/mainmenu.yaml
+++ b/mods/d2k/chrome/mainmenu.yaml
@@ -101,6 +101,13 @@ Container@MAINMENU:
Height: 30
Text: Missions
Font: Bold
+ Button@LOAD_BUTTON:
+ X: PARENT_RIGHT / 2 - WIDTH / 2
+ Y: 140
+ Width: 140
+ Height: 30
+ Text: Load
+ Font: Bold
Button@BACK_BUTTON:
X: PARENT_RIGHT / 2 - WIDTH / 2
Key: escape
diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml
index 3f065b3bd6..59382355ba 100644
--- a/mods/d2k/mod.yaml
+++ b/mods/d2k/mod.yaml
@@ -107,6 +107,7 @@ ChromeLayout:
common|chrome/confirmation-dialogs.yaml
common|chrome/editor.yaml
common|chrome/replaybrowser.yaml
+ common|chrome/gamesave-browser.yaml
common|chrome/gamesave-loading.yaml
Weapons:
diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml
index 0e68a5cc6a..a3f664d1c5 100644
--- a/mods/ra/mod.yaml
+++ b/mods/ra/mod.yaml
@@ -114,6 +114,7 @@ ChromeLayout:
common|chrome/multiplayer-directconnect.yaml
common|chrome/connection.yaml
common|chrome/replaybrowser.yaml
+ common|chrome/gamesave-browser.yaml
ra|chrome/gamesave-loading.yaml
common|chrome/dropdowns.yaml
common|chrome/musicplayer.yaml
diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml
index b674ee09e5..3ca217105a 100644
--- a/mods/ts/mod.yaml
+++ b/mods/ts/mod.yaml
@@ -163,6 +163,7 @@ ChromeLayout:
common|chrome/multiplayer-directconnect.yaml
common|chrome/connection.yaml
common|chrome/replaybrowser.yaml
+ common|chrome/gamesave-browser.yaml
common|chrome/gamesave-loading.yaml
ts|chrome/dropdowns.yaml
common|chrome/musicplayer.yaml