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