Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
425
OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
Normal file
425
OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
#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 = [];
|
||||
readonly Action onStart;
|
||||
readonly Action onExit;
|
||||
readonly ModData modData;
|
||||
readonly bool isSavePanel;
|
||||
readonly string baseSavePath;
|
||||
|
||||
readonly string defaultSaveFilename;
|
||||
string selectedPath;
|
||||
GameSave 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 = saveButton.HandleKeyPress;
|
||||
saveTextField.OnEscKey = cancelButton.HandleKeyPress;
|
||||
}
|
||||
else
|
||||
{
|
||||
panel.Get("LOAD_TITLE").IsVisible = () => true;
|
||||
var loadButton = panel.Get<ButtonWidget>("LOAD_BUTTON");
|
||||
loadButton.IsVisible = () => true;
|
||||
loadButton.IsDisabled = () => selectedSave == null || modData.MapCache[selectedSave.GlobalSettings.Map].Status != MapStatus.Available;
|
||||
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(selectedPath);
|
||||
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: ["save", Path.GetFileNameWithoutExtension(selectedPath)],
|
||||
onConfirm: () =>
|
||||
{
|
||||
Delete(selectedPath);
|
||||
|
||||
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: ["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(File.GetLastWriteTime)
|
||||
.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 = () => selectedPath == 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 (selectedPath == oldPath)
|
||||
selectedPath = 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 == selectedPath)
|
||||
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)
|
||||
{
|
||||
selectedPath = savePath;
|
||||
|
||||
if (savePath != null)
|
||||
{
|
||||
selectedSave = new GameSave(savePath);
|
||||
var map = modData.MapCache[selectedSave.GlobalSettings.Map];
|
||||
if (map.Status != MapStatus.Available && selectedSave.MapGenerationArgs != null)
|
||||
{
|
||||
// Add to the MapCache so the server will accept the map
|
||||
modData.MapCache.GenerateMap(modData, selectedSave.MapGenerationArgs);
|
||||
}
|
||||
}
|
||||
else
|
||||
selectedSave = null;
|
||||
|
||||
if (isSavePanel)
|
||||
{
|
||||
saveTextField.Text = savePath == null ? defaultSaveFilename : Path.GetFileNameWithoutExtension(savePath);
|
||||
saveTextField.CursorPosition = saveTextField.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
if (selectedSave == null)
|
||||
return;
|
||||
|
||||
var map = modData.MapCache[selectedSave.GlobalSettings.Map];
|
||||
if (map.Status != MapStatus.Available)
|
||||
return;
|
||||
|
||||
Ui.CloseWindow();
|
||||
|
||||
var orders = new List<Order>()
|
||||
{
|
||||
Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedPath), 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: ["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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user