Implement auto-save

This commit is contained in:
Dan Stoian
2025-01-16 18:29:19 +02:00
committed by Gustas
parent 94f2ce4f56
commit e903baf680
22 changed files with 392 additions and 1 deletions

View File

@@ -251,6 +251,14 @@ namespace OpenRA
public Color[] CustomColors = Array.Empty<Color>();
}
public class SinglePlayerGameSettings
{
[Desc("Sets the Auto-save frequency, in seconds")]
public int AutoSaveInterval = 0;
[Desc("Sets the AutoSave number of max files to bes saved on the file-system")]
public int AutoSaveMaxFileCount = 10;
}
public class GameSettings
{
public string Platform = "Default";
@@ -308,8 +316,8 @@ namespace OpenRA
public readonly GraphicSettings Graphics = new();
public readonly ServerSettings Server = new();
public readonly DebugSettings Debug = new();
public readonly SinglePlayerGameSettings SinglePlayerSettings = new();
internal Dictionary<string, Hotkey> Keys = new();
public readonly Dictionary<string, object> Sections;
// A direct clone of the file loaded from disk.
@@ -328,6 +336,7 @@ namespace OpenRA
{ "Graphics", Graphics },
{ "Server", Server },
{ "Debug", Debug },
{ "SinglePlayerSettings", SinglePlayerSettings },
};
// Override fieldloader to ignore invalid entries

View File

@@ -0,0 +1,99 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.Globalization;
using System.IO;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World)]
[Desc("Add this trait to the world actor to enable auto-save.")]
public class AutoSaveInfo : TraitInfo
{
public override object Create(ActorInitializer init) { return new AutoSave(init.Self, this); }
}
public class AutoSave : ITick
{
const string AutoSavePattern = "autosave-";
const string SaveFileExtension = ".orasav";
int ticksUntilAutoSave;
int lastSaveInverval;
readonly bool isDisabled;
public AutoSave(Actor self, AutoSaveInfo info)
{
ticksUntilAutoSave = GetTicksBetweenAutosaves(self);
lastSaveInverval = Game.Settings.SinglePlayerSettings.AutoSaveInterval;
isDisabled = self.World.LobbyInfo.GlobalSettings.Dedicated || self.World.LobbyInfo.NonBotClients.Count() > 1;
}
void ITick.Tick(Actor self)
{
if (isDisabled || self.World.IsReplay || self.World.IsLoadingGameSave)
return;
var autoSaveValue = Game.Settings.SinglePlayerSettings.AutoSaveInterval;
if (autoSaveValue == 0)
return;
var autoSaveFileLimit = Game.Settings.SinglePlayerSettings.AutoSaveMaxFileCount;
autoSaveFileLimit = autoSaveFileLimit < 3 ? 3 : autoSaveFileLimit;
if (lastSaveInverval != autoSaveValue)
{
lastSaveInverval = autoSaveValue;
ticksUntilAutoSave = GetTicksBetweenAutosaves(self);
}
if (--ticksUntilAutoSave > 0)
return;
var oldAutoSaveFiles = GetAutoSaveFiles()
.OrderByDescending(f => f.CreationTime)
.Skip(autoSaveFileLimit - 1);
foreach (var oldAutoSaveFile in oldAutoSaveFiles)
oldAutoSaveFile.Delete();
var dateTime = DateTime.UtcNow.ToString("yyyy-MM-ddTHHmmssZ", CultureInfo.InvariantCulture);
var fileName = $"{AutoSavePattern}{dateTime}{SaveFileExtension}";
self.World.RequestGameSave(fileName);
ticksUntilAutoSave = GetTicksBetweenAutosaves(self);
}
static IEnumerable<FileSystemInfo> GetAutoSaveFiles()
{
var mod = Game.ModData.Manifest;
var saveFolderPath = Path.Combine(Platform.SupportDir, "Saves", mod.Id, mod.Metadata.Version);
var autoSaveDirectoryInfo = new DirectoryInfo(saveFolderPath);
if (!autoSaveDirectoryInfo.Exists)
return Array.Empty<FileInfo>();
return autoSaveDirectoryInfo.EnumerateFiles($"{AutoSavePattern}*{SaveFileExtension}");
}
static int GetTicksBetweenAutosaves(Actor self)
{
return 1000 / self.World.Timestep * Game.Settings.SinglePlayerSettings.AutoSaveInterval;
}
}
}

View File

@@ -0,0 +1,127 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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 OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class GameplaySettingsLogic : ChromeLogic
{
[FluentReference]
const string AutoSaveIntervalOptions = "auto-save-interval.options";
[FluentReference]
const string AutoSaveIntervalDisabled = "auto-save-interval.disabled";
[FluentReference]
const string AutoSaveIntervalMinuteOptions = "auto-save-interval.minute-options";
[FluentReference]
const string AutoSaveMaxFileNumber = "auto-save-max-file-number";
readonly int[] autoSaveSeconds = { 0, 10, 30, 45, 60, 120, 180, 300, 600 };
readonly int[] autoSaveFileNumbers = { 3, 5, 10, 20, 50, 100 };
[ObjectCreator.UseCtor]
public GameplaySettingsLogic(Action<string, string, Func<Widget, Func<bool>>, Func<Widget, Action>> registerPanel, string panelID, string label)
{
registerPanel(panelID, label, InitPanel, ResetPanel);
}
Func<bool> InitPanel(Widget panel)
{
var scrollPanel = panel.Get<ScrollPanelWidget>("SETTINGS_SCROLLPANEL");
SettingsUtils.AdjustSettingsScrollPanelLayout(scrollPanel);
// Setup dropdown for auto-save interval
var autoSaveIntervalDropDown = panel.Get<DropDownButtonWidget>("AUTO_SAVE_INTERVAL_DROP_DOWN");
autoSaveIntervalDropDown.OnClick = () =>
ShowAutoSaveIntervalDropdown(autoSaveIntervalDropDown, autoSaveSeconds);
autoSaveIntervalDropDown.GetText = () => GetMessageForAutoSaveInterval(Game.Settings.SinglePlayerSettings.AutoSaveInterval);
// Setup dropdown for auto-save nr.
var autoSaveNoDropDown = panel.Get<DropDownButtonWidget>("AUTO_SAVE_FILE_NUMBER_DROP_DOWN");
autoSaveNoDropDown.OnMouseDown = _ =>
ShowAutoSaveFileNumberDropdown(autoSaveNoDropDown, autoSaveFileNumbers);
autoSaveNoDropDown.GetText = () => FluentProvider.GetMessage(AutoSaveMaxFileNumber, "saves", Game.Settings.SinglePlayerSettings.AutoSaveMaxFileCount);
autoSaveNoDropDown.IsDisabled = () => Game.Settings.SinglePlayerSettings.AutoSaveInterval <= 0;
return () => false;
}
Action ResetPanel(Widget panel)
{
return () => { };
}
void ShowAutoSaveIntervalDropdown(DropDownButtonWidget dropdown, IEnumerable<int> options)
{
var gsp = Game.Settings.SinglePlayerSettings;
ScrollItemWidget SetupItem(int o, ScrollItemWidget itemTemplate)
{
var item = ScrollItemWidget.Setup(itemTemplate,
() => gsp.AutoSaveInterval == o,
() =>
{
gsp.AutoSaveInterval = o;
Game.Settings.Save();
});
var deviceLabel = item.Get<LabelWidget>("LABEL");
deviceLabel.GetText = () => GetMessageForAutoSaveInterval(o);
return item;
}
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, options, SetupItem);
}
void ShowAutoSaveFileNumberDropdown(DropDownButtonWidget dropdown, IEnumerable<int> options)
{
var gsp = Game.Settings.SinglePlayerSettings;
ScrollItemWidget SetupItem(int o, ScrollItemWidget itemTemplate)
{
var item = ScrollItemWidget.Setup(itemTemplate,
() => gsp.AutoSaveMaxFileCount == o,
() =>
{
gsp.AutoSaveMaxFileCount = o;
Game.Settings.Save();
});
var deviceLabel = item.Get<LabelWidget>("LABEL");
deviceLabel.GetText = () => FluentProvider.GetMessage(AutoSaveMaxFileNumber, "saves", o);
return item;
}
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, options, SetupItem);
}
static string GetMessageForAutoSaveInterval(int value) =>
value switch
{
0 => FluentProvider.GetMessage(AutoSaveIntervalDisabled),
< 60 => FluentProvider.GetMessage(AutoSaveIntervalOptions, "seconds", value),
_ => FluentProvider.GetMessage(AutoSaveIntervalMinuteOptions, "minutes", value / 60)
};
}
}

View File

@@ -0,0 +1,59 @@
Container@GAMEPLAY_PANEL:
Logic: GameplaySettingsLogic
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
Children:
ScrollPanel@SETTINGS_SCROLLPANEL:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
CollapseHiddenChildren: True
TopBottomSpacing: 5
ItemSpacing: 10
Children:
Background@INPUT_SECTION_HEADER:
X: 5
Width: PARENT_WIDTH - 24 - 10
Height: 13
Background: separator
ClickThrough: True
Children:
Label@LABEL:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
Font: TinyBold
Align: Center
Text: label-game-play-section-header
Container@ROW:
Width: PARENT_WIDTH - 24
Height: 50
Children:
Container@AUTO_SAVE_INTERVAL_CONTAINER:
X: 10
Width: PARENT_WIDTH / 2 - 20
Children:
LabelForInput@AUTO_SAVE_INTERVAL_DROP_DOWN_LABEL:
Width: PARENT_WIDTH
Height: 20
Font: Regular
Text: auto-save-interval-label
For: AUTO_SAVE_INTERVAL_DROP_DOWN
DropDownButton@AUTO_SAVE_INTERVAL_DROP_DOWN:
Y: 25
Width: PARENT_WIDTH
Height: 25
Font: Regular
Container@AUTO_SAVE_NO_CONTAINER:
X: PARENT_WIDTH / 2 + 10
Width: PARENT_WIDTH / 2 - 20
Children:
LabelForInput@AUTO_SAVE_FILE_NUMBER_DROP_DOWN_LABEL:
Width: PARENT_WIDTH
Height: 20
Font: Regular
Text: auto-save-max-file-number-label
For: AUTO_SAVE_FILE_NUMBER_DROP_DOWN
DropDownButton@AUTO_SAVE_FILE_NUMBER_DROP_DOWN:
Y: 25
Width: PARENT_WIDTH
Height: 25
Font: Regular

View File

@@ -7,6 +7,7 @@ Container@SETTINGS_PANEL:
INPUT_PANEL: Input
HOTKEYS_PANEL: Hotkeys
ADVANCED_PANEL: Advanced
GAMEPLAY_PANEL: Gameplay
X: (WINDOW_WIDTH - WIDTH) / 2
Y: (WINDOW_HEIGHT - HEIGHT) / 2
Width: 640

View File

@@ -651,6 +651,11 @@ label-audio-device-container = Audio Device:
label-video-volume-container = Video Volume:
label-restart-required-container-audio-desc = Device changes will be applied after the game is restarted
## settings-gameplay.yaml
label-game-play-section-header = Auto-save
auto-save-interval-label = Auto-save frequency:
auto-save-max-file-number-label = Auto-save limit:
## settings-display.yaml
label-target-lines-dropdown-container = Target Lines:
label-status-bar-dropdown-container-bars = Status Bars:

View File

@@ -46,6 +46,7 @@ Rules:
-SpawnStartingUnits:
-MapStartingLocations:
-CrateSpawner:
-AutoSave:
MusicPlaylist:
BackgroundMusic: map1
AllowMuteBackgroundMusic: true

View File

@@ -133,6 +133,7 @@ ChromeLayout:
cnc|chrome/music.yaml
cnc|chrome/settings.yaml
cnc|chrome/settings-display.yaml
cnc|chrome/settings-gameplay.yaml
cnc|chrome/settings-audio.yaml
cnc|chrome/settings-input.yaml
cnc|chrome/settings-hotkeys.yaml

View File

@@ -154,6 +154,7 @@ World:
DevCommands:
DebugVisualizationCommands:
PathFinderOverlay:
AutoSave:
HierarchicalPathFinderOverlay:
PlayerCommands:
HelpCommand:

View File

@@ -0,0 +1,57 @@
Container@GAMEPLAY_PANEL:
Logic: GameplaySettingsLogic
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
Children:
ScrollPanel@SETTINGS_SCROLLPANEL:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
CollapseHiddenChildren: True
TopBottomSpacing: 5
ItemSpacing: 10
Children:
Background@INPUT_SECTION_HEADER:
X: 5
Width: PARENT_WIDTH - 24 - 10
Height: 13
Background: separator
ClickThrough: True
Children:
Label@LABEL:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
Font: TinyBold
Align: Center
Text: label-game-play-section-header
Container@ROW:
Width: PARENT_WIDTH - 24
Height: 50
Children:
Container@AUTO_SAVE_INTERVAL_CONTAINER:
X: 10
Width: PARENT_WIDTH / 2 - 20
Children:
Label@AUTO_SAVE_INTERVAL_DROP_DOWN_LABEL:
Width: PARENT_WIDTH
Height: 20
Font: Regular
Text: auto-save-interval-label
DropDownButton@AUTO_SAVE_INTERVAL_DROP_DOWN:
Y: 25
Width: PARENT_WIDTH
Height: 25
Font: Regular
Container@AUTO_SAVE_NO_CONTAINER:
X: PARENT_WIDTH / 2 + 10
Width: PARENT_WIDTH / 2 - 20
Children:
Label@AUTO_SAVE_FILE_NUMBER_DROP_DOWN_LABEL:
Width: PARENT_WIDTH
Height: 20
Font: Regular
Text: auto-save-nr-label
DropDownButton@AUTO_SAVE_FILE_NUMBER_DROP_DOWN:
Y: 25
Width: PARENT_WIDTH
Height: 25
Font: Regular

View File

@@ -7,6 +7,7 @@ Background@SETTINGS_PANEL:
INPUT_PANEL: Input
HOTKEYS_PANEL: Hotkeys
ADVANCED_PANEL: Advanced
GAMEPLAY_PANEL: Gameplay
X: (WINDOW_WIDTH - WIDTH) / 2
Y: (WINDOW_HEIGHT - HEIGHT) / 2
Width: 900

View File

@@ -473,6 +473,11 @@ label-audio-device-container = Audio Device:
label-video-volume-container = Video Volume:
label-restart-required-container-audio-desc = Device changes will be applied after the game is restarted
## settings-gameplay.yaml
label-game-play-section-header = Auto-save
auto-save-interval-label = Auto-save frequency:
auto-save-nr-label = Auto-save limit:
## settings-display.yaml
label-target-lines-dropdown-container = Target Lines:
label-status-bar-dropdown-container-bars = Status Bars:

View File

@@ -408,6 +408,22 @@ label-original-notice = The default is "{ $key }"
label-duplicate-notice = This is already used for "{ $key }" in the { $context } context
hotkey-context-any = Any
## GameplaySettingsLogic
auto-save-interval =
.disabled = Disabled
.options =
{ $seconds ->
[one] 1 second
*[other] { $seconds } seconds
}
.minute-options =
{ $minutes ->
[one] 1 minute
*[other] { $minutes } minutes
}
auto-save-max-file-number = { $saves } saves
## InputSettingsLogic
options-mouse-scroll-type =
.disabled = Disabled

View File

@@ -7,6 +7,7 @@ Player:
World:
-CrateSpawner:
-AutoSave:
-SpawnStartingUnits:
-MapStartingLocations:
ActorSpawnManager:

View File

@@ -100,6 +100,7 @@ ChromeLayout:
common|chrome/mainmenu-prompts.yaml
common|chrome/settings.yaml
common|chrome/settings-display.yaml
common|chrome/settings-gameplay.yaml
common|chrome/settings-audio.yaml
common|chrome/settings-input.yaml
common|chrome/settings-hotkeys.yaml

View File

@@ -125,6 +125,7 @@ World:
HierarchicalPathFinderOverlay:
PlayerCommands:
HelpCommand:
AutoSave:
ScreenShaker:
BuildingInfluence:
ProductionQueueFromSelection:

View File

@@ -10,6 +10,7 @@ Player:
World:
-CrateSpawner:
-AutoSave:
-SpawnStartingUnits:
-MapStartingLocations:
MusicPlaylist:

View File

@@ -137,6 +137,7 @@ ChromeLayout:
common|chrome/settings-audio.yaml
common|chrome/settings-input.yaml
common|chrome/settings-hotkeys.yaml
common|chrome/settings-gameplay.yaml
common|chrome/settings-advanced.yaml
common|chrome/credits.yaml
common|chrome/lobby.yaml

View File

@@ -276,6 +276,7 @@ World:
LoadWidgetAtGameStart:
ScriptTriggers:
CellTriggerOverlay:
AutoSave:
TimeLimitManager:
TimeLimitDisplayOrder: 2
TimeLimitWarnings:

View File

@@ -9,6 +9,7 @@ World:
-CrateSpawner:
-StartGameNotification:
-SpawnStartingUnits:
-AutoSave:
-MapStartingLocations:
LuaScript:
Scripts: fields-of-green.lua

View File

@@ -142,6 +142,7 @@ ChromeLayout:
ts|chrome/mainmenu-prerelease-notification.yaml
common|chrome/settings.yaml
common|chrome/settings-display.yaml
common|chrome/settings-gameplay.yaml
common|chrome/settings-audio.yaml
common|chrome/settings-input.yaml
ts|chrome/settings-hotkeys.yaml

View File

@@ -236,6 +236,7 @@ World:
Inherits: ^BaseWorld
ChatCommands:
DevCommands:
AutoSave:
DebugVisualizationCommands:
PathFinderOverlay:
HierarchicalPathFinderOverlay: