Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
RoosterDragon aa121dcb2f Use covariant return types.
Use the covariant return type feature of C# 9 to allow method overrides to be more specific about their return type. By exposing this information, e.g. for Widget.Clone(), callers will have more information. This allows various casts to be removed as the information is now available within the type system.
2025-01-15 17:29:02 +02:00

413 lines
11 KiB
C#

#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.Network;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class GameSaveBrowserLogic : ChromeLogic
{
[FluentReference]
const string RenameSaveTitle = "dialog-rename-save.title";
[FluentReference]
const string RenameSavePrompt = "dialog-rename-save.prompt";
[FluentReference]
const string RenameSaveAccept = "dialog-rename-save.confirm";
[FluentReference]
const string DeleteSaveTitle = "dialog-delete-save.title";
[FluentReference("save")]
const string DeleteSavePrompt = "dialog-delete-save.prompt";
[FluentReference]
const string DeleteSaveAccept = "dialog-delete-save.confirm";
[FluentReference]
const string DeleteAllSavesTitle = "dialog-delete-all-saves.title";
[FluentReference("count")]
const string DeleteAllSavesPrompt = "dialog-delete-all-saves.prompt";
[FluentReference]
const string DeleteAllSavesAccept = "dialog-delete-all-saves.confirm";
[FluentReference("savePath")]
const string SaveDeletionFailed = "notification-save-deletion-failed";
[FluentReference]
const string OverwriteSaveTitle = "dialog-overwrite-save.title";
[FluentReference("file")]
const string OverwriteSavePrompt = "dialog-overwrite-save.prompt";
[FluentReference]
const string OverwriteSaveAccept = "dialog-overwrite-save.confirm";
readonly Widget panel;
readonly ScrollPanelWidget gameList;
readonly TextFieldWidget saveTextField;
readonly List<string> games = new();
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;
var cancelButton = panel.Get<ButtonWidget>("CANCEL_BUTTON");
cancelButton.OnClick = () =>
{
Ui.CloseWindow();
onExit();
};
gameList = panel.Get<ScrollPanelWidget>("GAME_LIST");
var gameTemplate = panel.Get<ScrollItemWidget>("GAME_TEMPLATE");
var newTemplate = panel.Get<ScrollItemWidget>("NEW_TEMPLATE");
var mod = modData.Manifest;
baseSavePath = Path.Combine(Platform.SupportDir, "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 (File.Exists(Path.Combine(baseSavePath, defaultSaveFilename + ".orasav")))
defaultSaveFilename = world.Map.Title + $" ({++filenameAttempt})";
var saveButton = panel.Get<ButtonWidget>("SAVE_BUTTON");
saveButton.IsDisabled = () => string.IsNullOrWhiteSpace(saveTextField.Text);
saveButton.OnClick = () => Save(world);
saveButton.IsVisible = () => true;
var saveWidgets = panel.Get("SAVE_WIDGETS");
gameList.Bounds.Height -= saveWidgets.Bounds.Height;
saveWidgets.IsVisible = () => true;
saveTextField = saveWidgets.Get<TextFieldWidget>("SAVE_TEXTFIELD");
saveTextField.OnEnterKey = input => saveButton.HandleKeyPress(input);
saveTextField.OnEscKey = input => cancelButton.HandleKeyPress(input);
}
else
{
panel.Get("LOAD_TITLE").IsVisible = () => true;
var loadButton = panel.Get<ButtonWidget>("LOAD_BUTTON");
loadButton.IsVisible = () => true;
loadButton.IsDisabled = () => selectedSave == null;
loadButton.OnClick = Load;
}
if (Directory.Exists(baseSavePath))
LoadGames(gameTemplate, newTemplate, world);
var renameButton = panel.Get<ButtonWidget>("RENAME_BUTTON");
renameButton.IsDisabled = () => selectedSave == null;
renameButton.OnClick = () =>
{
var initialName = Path.GetFileNameWithoutExtension(selectedSave);
var invalidChars = Path.GetInvalidFileNameChars();
ConfirmationDialogs.TextInputPrompt(modData,
RenameSaveTitle,
RenameSavePrompt,
initialName,
onAccept: newName => Rename(initialName, newName),
onCancel: null,
acceptText: RenameSaveAccept,
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<ButtonWidget>("DELETE_BUTTON");
deleteButton.IsDisabled = () => selectedSave == null;
deleteButton.OnClick = () =>
{
ConfirmationDialogs.ButtonPrompt(modData,
title: DeleteSaveTitle,
text: DeleteSavePrompt,
textArguments: new object[] { "save", Path.GetFileNameWithoutExtension(selectedSave) },
onConfirm: () =>
{
Delete(selectedSave);
if (games.Count == 0 && !isSavePanel)
{
Ui.CloseWindow();
onExit();
}
else
SelectFirstVisible();
},
confirmText: DeleteSaveAccept,
onCancel: () => { });
};
var deleteAllButton = panel.Get<ButtonWidget>("DELETE_ALL_BUTTON");
deleteAllButton.IsDisabled = () => games.Count == 0;
deleteAllButton.OnClick = () =>
{
ConfirmationDialogs.ButtonPrompt(modData,
title: DeleteAllSavesTitle,
text: DeleteAllSavesPrompt,
textArguments: new object[] { "count", games.Count },
onConfirm: () =>
{
foreach (var s in games.ToList())
Delete(s);
Ui.CloseWindow();
onExit();
},
confirmText: DeleteAllSavesAccept,
onCancel: () => { });
};
SelectFirstVisible();
}
void LoadGames(ScrollItemWidget gameTemplate, ScrollItemWidget newTemplate, World world)
{
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();
item.ItemKey = savePath;
item.IsVisible = () => true;
item.IsSelected = () => selectedSave == item.ItemKey;
item.OnClick = () => Select(item.ItemKey);
if (isSavePanel)
item.OnDoubleClick = () => Save(world);
else
item.OnDoubleClick = Load;
var title = Path.GetFileNameWithoutExtension(savePath);
var label = item.Get<LabelWithTooltipWidget>("TITLE");
WidgetUtils.TruncateLabelToTooltip(label, title);
var date = File.GetLastWriteTime(savePath).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture);
item.Get<LabelWidget>("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)
{
if (c is not ScrollItemWidget item || item.ItemKey != oldPath)
continue;
item.ItemKey = newPath;
item.Get<LabelWidget>("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)
{
TextNotificationsManager.Debug(FluentProvider.GetMessage(SaveDeletionFailed, "savePath", 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());
if (isSavePanel)
{
saveTextField.TakeKeyboardFocus();
saveTextField.CursorPosition = saveTextField.Text.Length;
}
}
void Select(string savePath)
{
selectedSave = savePath;
if (isSavePanel)
{
saveTextField.Text = savePath == null ? defaultSaveFilename : Path.GetFileNameWithoutExtension(savePath);
saveTextField.CursorPosition = saveTextField.Text.Length;
}
}
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;
Ui.CloseWindow();
var orders = new List<Order>()
{
Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedSave), true),
Order.Command($"state {Session.ClientState.Ready}")
};
Game.CreateAndStartLocalServer(map.Uid, orders);
}
void Save(World world)
{
var filename = saveTextField.Text + ".orasav";
var testPath = Path.Combine(
Platform.SupportDir,
"Saves",
modData.Manifest.Id,
modData.Manifest.Metadata.Version,
filename);
void Inner()
{
world.RequestGameSave(filename);
Ui.CloseWindow();
onExit();
}
if (File.Exists(testPath))
{
ConfirmationDialogs.ButtonPrompt(modData,
title: OverwriteSaveTitle,
text: OverwriteSavePrompt,
textArguments: new object[] { "file", saveTextField.Text },
onConfirm: Inner,
confirmText: OverwriteSaveAccept,
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 = Path.Combine(Platform.SupportDir, "Saves", mod.Id, mod.Metadata.Version);
if (!Directory.Exists(baseSavePath))
return false;
return Directory.GetFiles(baseSavePath, "*.orasav", SearchOption.AllDirectories).Length > 0;
}
}
}