Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
2024-11-03 16:52:47 +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() as ScrollItemWidget;
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;
}
}
}