diff --git a/Content/Text/English.json b/Content/Text/English.json index e68fd0cf..8b310ba2 100644 --- a/Content/Text/English.json +++ b/Content/Text/English.json @@ -14,6 +14,8 @@ "Create": "Create", "Delete": "Delete", "Copy": "Copy", + "Rename": "Rename", + "NewName": "New Name", "Bind": "Bind", "Reset": "Reset", "Clear": "Clear", @@ -95,8 +97,11 @@ "Skin.desc": "Choose a Skin to Wear", "SaveCreateFile": "Create new file?", "SaveDeleteFile": "Delete this file?", - "SaveDeleteDefaultFile": "Cannot fully delete default save file:'{0}'. Reset it to default instead?", + "SaveDeleteDefaultFile": "The file {0} cannot be deleted.\n\nThis may happen because the file is the only existing file,\nor because it is currently loaded.\n\nDo you want to reset to the default file instead?", "SaveCopyFile": "Copy this file?", + "SaveRenameFile": "Renaming file: {0}", + "SaveRenameFailed": "Failed to rename... Check your log file for details\n(does the file already exist?)", + "OSKUserInstructions": "Press Enter on keyboard, or Back on controller to confirm.", "FujiOptions": "Fuji Options...", "FujiOptions.desc": "Configure Options for the Fuji Mod Loader", "FujiWriteLog": "Write Log File", @@ -122,7 +127,8 @@ "ModOptions": "Mod Options", "ConfirmDisableMod": "Continue & Disable Mod", "ModSafeDisableErrorMessage": "This mod can't be disabled because it would disable the level you're currently in.\nTry going to the main menu first.", - "QuitToMainMenu": "Quit to Main Menu" + "QuitToMainMenu": "Quit to Main Menu", + "KeyboardSpace": "Space" }, "Dialog": { "Granny1": [ diff --git a/Source/Data/Controls.cs b/Source/Data/Controls.cs index 7a1ecf8e..9a288ae7 100644 --- a/Source/Data/Controls.cs +++ b/Source/Data/Controls.cs @@ -6,7 +6,6 @@ namespace Celeste64; [DisallowHooks] public static class Controls { - #region Default Controls [DefaultStickBinding(StickDirection.Up, Keys.Up)] [DefaultStickBinding(StickDirection.Up, Buttons.Up)] @@ -95,6 +94,11 @@ public static class Controls [DefaultBinding(Buttons.RightShoulder)] public static VirtualButton CreateFile { get; private set; } = new("CreateFile"); + + [DefaultBinding(Keys.R)] + [DefaultBinding(Buttons.West)] + public static VirtualButton RenameFile { get; private set; } = new("RenameFile"); + [DefaultBinding(Keys.V)] [DefaultBinding(Buttons.LeftShoulder)] public static VirtualButton ResetBindings { get; private set; } = new("ResetBindings"); @@ -503,7 +507,6 @@ internal static void ResetBinding(VirtualButton virtualButton, bool forControlle internal static void ResetAllBindings(bool forController, GameMod? mod = null) { ControlsConfig_V01 config; - IEnumerable defaultBindings; Type settingsType; object? settingsObject; diff --git a/Source/Data/Save.cs b/Source/Data/Save.cs index 1a58dc63..7d8e0dfa 100644 --- a/Source/Data/Save.cs +++ b/Source/Data/Save.cs @@ -114,8 +114,8 @@ public static SkinInfo GetSkin() public static void SaveToFile() { - var savePath = Path.Join(App.UserPath, Instance.FileName); - var tempPath = Path.Join(App.UserPath, Instance.FileName + ".backup"); + var savePath = Path.Join(App.UserPath, "Saves", Instance.FileName); + var tempPath = Path.Join(App.UserPath, "Saves", Instance.FileName + ".backup"); // first save to a temporary file { @@ -135,7 +135,7 @@ public static void SaveToFile() internal static void LoadSaveByFileName(string fileName) { if (fileName == string.Empty) fileName = DefaultFileName; - var saveFile = Path.Join(App.UserPath, fileName); + var saveFile = Path.Join(App.UserPath, "Saves", fileName); if (File.Exists(saveFile)) Instance = Instance.Deserialize(File.ReadAllText(saveFile)) ?? new(); diff --git a/Source/Helpers/Menu.cs b/Source/Helpers/Menu.cs index 218a7244..36f08110 100644 --- a/Source/Helpers/Menu.cs +++ b/Source/Helpers/Menu.cs @@ -286,6 +286,47 @@ private static List GetEnumOptions() } } + public class InputField : Item + { + public OnScreenKeyboardMenu keyboardMenu; + + private Action setter; + private Func getter; + + public Menu RootMenu { get; protected set; } + + private string fieldText; + public override string Label => $"{LocString} : {getter()}"; + + public void SetFieldText(string text) + { + fieldText = text; + setter(fieldText); + } + + public string GetFieldText() + { + return getter(); + } + + public override bool Pressed() + { + RootMenu.PushSubMenu(keyboardMenu); + return true; + } + + public InputField(Loc.Localized locString, Action set, Func get, Menu rootMenu, Dictionary? characters = null) + { + LocString = locString; + setter = set; + getter = get; + RootMenu = rootMenu; + fieldText = getter(); + if (characters == null) characters = KeyboardHandler.AllCharactersList; + keyboardMenu = new OnScreenKeyboardMenu(rootMenu, this, characters); + } + } + public int Index; public string Title = string.Empty; public bool Focused = true; @@ -298,7 +339,7 @@ private static List GetEnumOptions() public string DownSound = Sfx.ui_move; public bool IsInMainMenu => submenus.Count <= 0; - protected Menu CurrentMenu => GetDeepestActiveSubmenu(this); + public Menu CurrentMenu => GetDeepestActiveSubmenu(this); protected virtual int maxItemsCount { get; set; } = 12; protected int scrolledAmount = 0; diff --git a/Source/Mod/Data/SaveManager.cs b/Source/Mod/Data/SaveManager.cs index b88dbf92..c9aa553f 100644 --- a/Source/Mod/Data/SaveManager.cs +++ b/Source/Mod/Data/SaveManager.cs @@ -4,14 +4,14 @@ internal sealed class SaveManager { internal static SaveManager Instance = new(); - [DisallowHooks] internal string GetLastLoadedSave() { - if (File.Exists(Path.Join(App.UserPath, "save.metadata"))) - return File.ReadAllText(Path.Join(App.UserPath, "save.metadata")); + if (File.Exists(Path.Join(App.UserPath, "Saves", "save.metadata"))) + return File.ReadAllText(Path.Join(App.UserPath, "Saves", "save.metadata")); else { - File.WriteAllText(Path.Join(App.UserPath, "save.metadata"), Save.DefaultFileName); + Directory.CreateDirectory(Path.Join(App.UserPath, "Saves")); // Perform upgrade path for first-time launch + File.WriteAllText(Path.Join(App.UserPath, "Saves", "save.metadata"), Save.DefaultFileName); return Save.DefaultFileName; } } @@ -19,41 +19,50 @@ internal string GetLastLoadedSave() [DisallowHooks] internal void SetLastLoadedSave(string save_name) { - if (File.Exists(Path.Join(App.UserPath, "save.metadata"))) - File.WriteAllText(Path.Join(App.UserPath, "save.metadata"), save_name); + if (File.Exists(Path.Join(App.UserPath, "Saves", "save.metadata"))) + File.WriteAllText(Path.Join(App.UserPath, "Saves", "save.metadata"), save_name); } - [DisallowHooks] internal List GetSaves() { List saves = new List(); - foreach (string savefile in Directory.GetFiles(App.UserPath)) + foreach (string file in Directory.GetFiles(Path.Join(App.UserPath))) + { + string saveFileName = Path.GetFileName(file); + if (saveFileName.StartsWith("save") && saveFileName.EndsWith(".json")) + { + if (saveFileName == "save.json" && !Path.Exists(Path.Join(App.UserPath, "Saves", saveFileName))) + File.Copy(Path.Join(App.UserPath, saveFileName), Path.Join(App.UserPath, "Saves", saveFileName)); + else if (!Path.Exists(Path.Join(App.UserPath, "Saves", saveFileName))) + File.Move(Path.Join(App.UserPath, saveFileName), Path.Join(App.UserPath, "Saves", saveFileName)); + } + } + + foreach (string savefile in Directory.GetFiles(Path.Join(App.UserPath, "Saves"))) { var saveFileName = Path.GetFileName(savefile); - if (saveFileName.EndsWith(".json") && saveFileName.StartsWith("save")) + if (saveFileName.EndsWith(".json")) saves.Add(saveFileName); } return saves; } - [DisallowHooks] internal void CopySave(string filename) { - if (File.Exists(Path.Join(App.UserPath, filename))) + if (File.Exists(Path.Join(App.UserPath, "Saves", filename))) { string new_file_name = $"{filename.Split(".json")[0]}(copy).json"; - File.Copy(Path.Join(App.UserPath, filename), Path.Join(App.UserPath, new_file_name)); + File.Copy(Path.Join(App.UserPath, "Saves", filename), Path.Join(App.UserPath, "Saves", new_file_name)); } } - [DisallowHooks] internal void NewSave(string? name = null) { if (string.IsNullOrEmpty(name)) name = $"save_{GetSaveCount()}.json"; - var savePath = Path.Join(App.UserPath, name); - var tempPath = Path.Join(App.UserPath, name + ".backup"); + var savePath = Path.Join(App.UserPath, "Saves", name); + var tempPath = Path.Join(App.UserPath, "Saves", name + ".backup"); // first save to a temporary file { @@ -69,18 +78,45 @@ internal void NewSave(string? name = null) } } - [DisallowHooks] + internal bool ChangeFileName(string originalFileName, string newFileName) + { + bool success = true; + + foreach (string file in GetSaves()) + { + if (file == originalFileName) + { + try + { + if (!newFileName.EndsWith(".json")) + newFileName += ".json"; + File.Move(Path.Join(App.UserPath, "Saves", file), Path.Join(App.UserPath, "Saves", newFileName)); + if (file == Save.Instance.FileName) + LoadSaveByFileName(newFileName); + } + catch (Exception e) + { + Log.Error($"Failed to rename save file {originalFileName} to {newFileName}"); + Log.Error(e.ToString()); + + success = false; + } + } + } + + return success; + } + internal int GetSaveCount() { return GetSaves().Count; } - [DisallowHooks] internal void DeleteSave(string save) { - if (File.Exists(Path.Join(App.UserPath, save))) + if (File.Exists(Path.Join(App.UserPath, "Saves", save))) { - File.Delete(Path.Join(App.UserPath, save)); + File.Delete(Path.Join(App.UserPath, "Saves", save)); } if (save == Save.DefaultFileName) @@ -89,9 +125,12 @@ internal void DeleteSave(string save) } } - [DisallowHooks] internal void LoadSaveByFileName(string fileName) { + if (!GetSaves().Contains(fileName)) // Make file if it doesn't exist yet + { + NewSave(fileName); + } Save.LoadSaveByFileName(fileName); Instance.SetLastLoadedSave(fileName); } diff --git a/Source/Mod/Menu/ControlsMenu.cs b/Source/Mod/Menu/ControlsMenu.cs index a4b10ff2..1cbea5ee 100644 --- a/Source/Mod/Menu/ControlsMenu.cs +++ b/Source/Mod/Menu/ControlsMenu.cs @@ -46,6 +46,7 @@ public ControlsMenu(Menu? rootMenu, bool isForController) Add(new InputBind(Controls.CopyFile.Name, Controls.CopyFile, rootMenu, isForController) { RequiresBinding = true }); Add(new InputBind(Controls.CreateFile.Name, Controls.CreateFile, rootMenu, isForController) { RequiresBinding = true }); Add(new InputBind(Controls.DeleteFile.Name, Controls.DeleteFile, rootMenu, isForController) { RequiresBinding = true }); + Add(new InputBind(Controls.RenameFile.Name, Controls.RenameFile, rootMenu, isForController) { RequiresBinding = true }); Add(new InputBind(Controls.ResetBindings.Name, Controls.ResetBindings, rootMenu, isForController) { RequiresBinding = true }); Add(new InputBind(Controls.ClearBindings.Name, Controls.ClearBindings, rootMenu, isForController) { RequiresBinding = true }); Add(new InputBind(Controls.Menu.Name + "Up", Controls.Menu.Vertical.Negative, rootMenu, isForController) { DeadZone = 0.5f, RequiresBinding = true }); diff --git a/Source/Mod/Menu/OnScreenKeyboardMenu.cs b/Source/Mod/Menu/OnScreenKeyboardMenu.cs new file mode 100644 index 00000000..0775b72d --- /dev/null +++ b/Source/Mod/Menu/OnScreenKeyboardMenu.cs @@ -0,0 +1,233 @@ +using System.ComponentModel; + +namespace Celeste64.Mod; + +public class OnScreenKeyboardMenu : Menu +{ + public Target Target; + public Target GameTarget; + + private int currentPage = 0; + private int currentRow = 0; + private int currentColumn = 0; + + private const int rows = 7; + private const int columns = 9; + + private int CurrentPageStart => currentPage * columns * rows; + private int CurrentIndex => currentRow * columns + currentColumn; + + private bool shiftMode; + private bool allowShift; + + private string textValue = string.Empty; + + private InputField Owner; + + private Dictionary KeyValueType; + private Dictionary KeyValueShiftType; + + private List KeyValueTypeList; + private List KeyValueShiftTypeList; + + internal OnScreenKeyboardMenu(Menu? rootMenu, InputField owner, Dictionary keyboardType, bool allowShiftMode = true) + { + Target = new Target(Overworld.CardWidth, Overworld.CardHeight); + Game.OnResolutionChanged += () => Target = new Target(Overworld.CardWidth, Overworld.CardHeight); + GameTarget = new Target(Game.Width, Game.Height); + RootMenu = rootMenu; + + KeyValueType = keyboardType; + KeyValueShiftType = KeyboardHandler.GetShiftDict(KeyValueType); + + KeyValueTypeList = KeyboardHandler.KeyValuesToList(KeyValueType); + KeyValueShiftTypeList = KeyboardHandler.KeyValuesToList(KeyValueShiftType); + + allowShift = allowShiftMode; + + Owner = owner; + } + + public override void Initialized() + { + base.Initialized(); + currentColumn = 0; + currentRow = 0; + currentPage = 0; + + textValue = Owner.GetFieldText(); + } + + public override void Closed() + { + Owner.SetFieldText(textValue); + } + + private void RenderCharacter(Batcher batch, string character, Vec2 pos, Vec2 size) + { + if (character == " ") + character = Loc.Str("KeyboardSpace"); + batch.PushMatrix(Matrix3x2.CreateScale(1.1f) * Matrix3x2.CreateTranslation((pos + new Vec2(size.X * 0.4f - 20, size.Y * 0.4f - 20)) * Game.RelativeScale)); + batch.Text(Language.Current.SpriteFont, character, Vec2.Zero, new Vec2(0.5f, 0), Color.Black); + + batch.PopMatrix(); + + batch.PushMatrix(Matrix3x2.CreateScale(0.9f) * Matrix3x2.CreateTranslation((pos + new Vec2(size.X * 0.4f - 20, size.Y * 0.4f - 20)) * Game.RelativeScale)); + batch.Text(Language.Current.SpriteFont, character, Vec2.Zero, new Vec2(0.5f, 0), Color.White); + + batch.PopMatrix(); + } + + private void RenderCurrentCharacter(Batcher batch, string character, Vec2 pos, Vec2 size) + { + if (character == " ") + character = Loc.Str("KeyboardSpace"); + batch.PushMatrix(Matrix3x2.CreateScale(1.1f) * Matrix3x2.CreateTranslation((pos + new Vec2(size.X * 0.4f - 20, size.Y * 0.4f - 20)) * Game.RelativeScale)); + batch.Text(Language.Current.SpriteFont, character, Vec2.Zero, new Vec2(0.5f, 0), Color.Black); + + batch.PopMatrix(); + + batch.PushMatrix(Matrix3x2.CreateScale(0.9f) * Matrix3x2.CreateTranslation((pos + new Vec2(size.X * 0.4f - 20, size.Y * 0.4f - 20)) * Game.RelativeScale)); + batch.Text(Language.Current.SpriteFont, character, Vec2.Zero, new Vec2(0.5f, 0), (Time.BetweenInterval(0.1f) ? 0x84FF54 : 0xFCFF59)); + + batch.PopMatrix(); + } + + private void RenderCharacters(Batcher batch) + { + var bounds = GameTarget.Bounds; + Vec2 size = new Vec2(bounds.Width, bounds.Height); + Vec2 offset = new Vec2(20, 20); + + int index = 0; + for (int i = 0; i < rows && CurrentPageStart + index < (KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count); i++) + { + for (int j = 0; j < columns && CurrentPageStart + index < (KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count); j++) + { + if (index == currentRow * columns + currentColumn) + { + string keyName = KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList)[CurrentPageStart + index]; + RenderCurrentCharacter(batch, keyName, new Vec2(j, i) * offset, size); + } + else + { + string keyName = KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList)[CurrentPageStart + index]; + RenderCharacter(batch, keyName, new Vec2(j, i) * offset, size); + } + index++; + } + } + } + + protected override void HandleInput() + { + if (Controls.Menu.Horizontal.Positive.Pressed) + { + if (currentColumn == columns - 1) + { + if (((currentPage + 1) * columns * rows) + (currentRow * columns) < KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count) + { + currentPage++; + currentColumn = 0; + } + else if ((currentPage + 1) * columns * rows < KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count) + { + currentPage++; + currentColumn = 0; + currentRow = 0; + } + } + else if (CurrentPageStart + CurrentIndex + 1 < KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count) + { + currentColumn += 1; + } + Audio.Play(DownSound); + } + if (Controls.Menu.Horizontal.Negative.Pressed) + { + if (currentColumn == 0) + { + if (currentPage > 0) + { + currentPage--; + currentColumn = columns - 1; + } + } + else + { + currentColumn -= 1; + } + Audio.Play(DownSound); + } + + if (Controls.Menu.Vertical.Positive.Pressed && (currentRow + 1) < rows && CurrentPageStart + CurrentIndex + columns < KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList).Count) + { + currentRow += 1; + Audio.Play(DownSound); + } + if (Controls.Menu.Vertical.Negative.Pressed && (currentRow - 1) >= 0) + { + currentRow -= 1; + Audio.Play(DownSound); + } + /* + * The next four controls are only relevant to gamepads. + * We check if a keyboard key is pressed to avoid conflicts. + */ + if (Controls.CopyFile.Pressed && KeyboardHandler.Instance.GetPressedKey() == null && allowShift) + shiftMode = !shiftMode; + + if (Controls.RenameFile.Pressed && textValue.Length > 0 && KeyboardHandler.Instance.GetPressedKey() == null) + { + textValue = textValue.Remove(textValue.Length - 1); + } + + if (Controls.Confirm.Pressed && KeyboardHandler.Instance.GetPressedKey() == null) + { + textValue += KeyboardHandler.TrimKeyList(!shiftMode ? KeyValueTypeList : KeyValueShiftTypeList)[CurrentPageStart + CurrentIndex]; + Audio.Play(UpSound); + } + + if (Controls.Cancel.ConsumePress() && KeyboardHandler.Instance.GetPressedKey() == null) + { + Owner.RootMenu.PopSubMenu(); + } + + if (KeyboardHandler.Instance.GetPressedKey() is Keys.Enter or Keys.Enter2 or Keys.KeypadEnter) + { + Owner.RootMenu.PopSubMenu(); + } + + ReadKey(); + } + + protected override void RenderItems(Batcher batch) + { + batch.PushMatrix(new Vec2(GameTarget.Bounds.TopLeft.X, GameTarget.Bounds.TopLeft.Y), false); + + if (textValue.Length == 0) + batch.Text(Language.Current.SpriteFont, "Enter text", new Vec2(GameTarget.Bounds.TopCenter.X - Language.Current.SpriteFont.WidthOf("Enter text") / 2, GameTarget.Bounds.TopCenter.Y + 96), Color.CornflowerBlue * 0.6f); + + batch.Text(Language.Current.SpriteFont, textValue, new Vec2(GameTarget.Bounds.TopCenter.X - Language.Current.SpriteFont.WidthOf(textValue) / 2, GameTarget.Bounds.TopCenter.Y + 96), Color.CornflowerBlue); + batch.Text(Language.Current.SpriteFont, Loc.Str("OSKUserInstructions"), new Vec2(GameTarget.Bounds.TopCenter.X - Language.Current.SpriteFont.WidthOf(Loc.Str("OSKUserInstructions")) / 2, GameTarget.Bounds.TopCenter.Y + 284), Color.CornflowerBlue); + RenderCharacters(batch); + } + + public void ReadKey() + { + Keys? key = KeyboardHandler.Instance.GetPressedKey(); + + if (key != null) + { + if (key == Keys.Backspace || key == Keys.KeypadBackspace) + { + if (textValue.Length > 0) + textValue = textValue.Remove(textValue.Length - 1); + } + else if (KeyValueTypeList.Contains(KeyboardHandler.GetKeyName(key))) + { + textValue += KeyboardHandler.GetKeyName(key); + } + } + } +} diff --git a/Source/Mod/Menu/SaveSelectionMenu.cs b/Source/Mod/Menu/SaveSelectionMenu.cs index f89c68dd..de84edbc 100644 --- a/Source/Mod/Menu/SaveSelectionMenu.cs +++ b/Source/Mod/Menu/SaveSelectionMenu.cs @@ -1,4 +1,5 @@ -using Celeste64.Mod.Data; +using Celeste64.Mod; +using Celeste64.Mod.Data; namespace Celeste64; @@ -21,6 +22,7 @@ public class SaveSelectionMenu : Menu private Subtexture strawberryImage; private List saves; + private string renamedFileName = string.Empty; internal SaveSelectionMenu(Menu? rootMenu) { @@ -108,21 +110,31 @@ private void RenderSaves(Batcher batch) { for (int j = 0; j < columns && CurrentPageStart + index < saves.Count; j++) { - if (saves[CurrentPageStart + index] == Save.Instance.FileName && index == currentRow * columns + currentColumn) RenderCurrentSelectedSave(batch, saves[CurrentPageStart + index], new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); - else if (saves[CurrentPageStart + index] == Save.Instance.FileName) RenderSelectedSave(batch, saves[CurrentPageStart + index], new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); + if (saves[CurrentPageStart + index] == Save.Instance.FileName && index == currentRow * columns + currentColumn) RenderCurrentSelectedSave(batch, saves[CurrentPageStart + index].Replace(".json", string.Empty), new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); + else if (saves[CurrentPageStart + index] == Save.Instance.FileName) RenderSelectedSave(batch, saves[CurrentPageStart + index].Replace(".json", string.Empty), new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); else if (index == currentRow * columns + currentColumn) { - RenderCurrentSave(batch, saves[CurrentPageStart + index], new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); + RenderCurrentSave(batch, saves[CurrentPageStart + index].Replace(".json", string.Empty), new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); } else { - RenderSave(batch, saves[CurrentPageStart + index], new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); + RenderSave(batch, saves[CurrentPageStart + index].Replace(".json", string.Empty), new Vec2(sizeMin * j * 1.1f, sizeMin * i * 1.1f) + offset, size); } index++; } } } + private void SetRename(string name) + { + renamedFileName = name; + } + + private string GetRename() + { + return renamedFileName; + } + protected override void HandleInput() { @@ -204,7 +216,7 @@ protected override void HandleInput() if (Controls.DeleteFile.Pressed) { Menu newMenu = new Menu(this); - if (saves[CurrentPageStart + CurrentIndex] == Save.DefaultFileName) + if (saves.Count == 1 || saves[CurrentPageStart + CurrentIndex] == Save.Instance.FileName) { newMenu.Title = string.Format(Loc.Str("SaveDeleteDefaultFile"), saves[CurrentPageStart + CurrentIndex]); } @@ -216,9 +228,9 @@ protected override void HandleInput() { if (Game.Instance.IsMidTransition) return; SaveManager.Instance.DeleteSave(saves[CurrentPageStart + CurrentIndex]); - if (saves[CurrentPageStart + CurrentIndex] == Save.Instance.FileName) + if ((saves[CurrentPageStart + CurrentIndex] == Save.Instance.FileName) || (saves.Count == 1)) { - // If we delete the current save, load default and force a reload + // If we delete the current save or the last remaining save, load default and force a reload SaveManager.Instance.LoadSaveByFileName(Save.DefaultFileName); Game.Instance.Goto(new Transition() { @@ -247,8 +259,38 @@ protected override void HandleInput() ResetSaves(); PopSubMenu(); })); - PushSubMenu(newMenu); newMenu.Add(new Option("OptionsNo", () => PopSubMenu())); + PushSubMenu(newMenu); + } + + if (Controls.RenameFile.Pressed) + { + Menu newMenu = new Menu(this); + newMenu.Title = string.Format(Loc.Str("SaveRenameFile"), saves[CurrentPageStart + CurrentIndex]); + Dictionary characters = KeyboardHandler.AlphabetValues.Concat(KeyboardHandler.NumberRowValues).ToDictionary(); + characters.Add("Space", " "); + newMenu.Add(new InputField( + "NewName", + (k) => SetRename(k), + GetRename, + newMenu, characters)); + newMenu.Add(new Option("OptionsYes", () => + { + bool renameSuccess = SaveManager.Instance.ChangeFileName(saves[CurrentPageStart + CurrentIndex], renamedFileName); + if (!renameSuccess) + { + Title = new Loc.Localized("SaveRenameFailed"); + } + ResetSaves(); + PopSubMenu(); + renamedFileName = string.Empty; + })); + newMenu.Add(new Option("OptionsNo", () => + { + renamedFileName = string.Empty; + PopSubMenu(); + })); + PushSubMenu(newMenu); } } @@ -280,6 +322,10 @@ protected override void RenderItems(Batcher batch) at.X -= width + 8 * Game.RelativeScale; UI.Prompt(batch, Controls.CopyFile, Loc.Str("Copy"), at, out width, 1.0f); + at.X -= width + 8 * Game.RelativeScale; + + UI.Prompt(batch, Controls.RenameFile, Loc.Str("Rename"), at, out _, 1.0f); + batch.PopMatrix(); } } diff --git a/Source/Mod/Patches/KeyboardHandler.cs b/Source/Mod/Patches/KeyboardHandler.cs new file mode 100644 index 00000000..6662fe9b --- /dev/null +++ b/Source/Mod/Patches/KeyboardHandler.cs @@ -0,0 +1,345 @@ +namespace Celeste64.Mod; + +class KeyboardHandler +{ + public static readonly Dictionary AllCharactersList = new Dictionary + { + { "A", "a" }, + { "B", "b" }, + { "C", "c" }, + { "D", "d" }, + { "E", "e" }, + { "F", "f" }, + { "G", "g" }, + { "H", "h" }, + { "I", "i" }, + { "J", "j" }, + { "K", "k" }, + { "L", "l" }, + { "M", "m" }, + { "N", "n" }, + { "O", "o" }, + { "P", "p" }, + { "Q", "q" }, + { "R", "r" }, + { "S", "s" }, + { "T", "t" }, + { "U", "u" }, + { "V", "v" }, + { "W", "w" }, + { "X", "x" }, + { "Y", "y" }, + { "Z", "z" }, + { "D1", "1" }, + { "D2", "2" }, + { "D3", "3" }, + { "D4", "4" }, + { "D5", "5" }, + { "D6", "6" }, + { "D7", "7" }, + { "D8", "8" }, + { "D9", "9" }, + { "D0", "0" }, + { "Space", " " }, + { "Minus", "-" }, + { "Equals", "=" }, + { "LeftBracket", "[" }, + { "RightBracket", "]" }, + { "Backslash", "\\" }, + { "Semicolon", ";" }, + { "Apostrophe", "'" }, + { "Tilde", "~" }, + { "Comma", "," }, + { "Period", "." }, + { "Slash", "/" }, + { "Keypad0", "0" }, + { "Keypad00", "00" }, + { "Keypad000", "000" }, + { "Keypad1", "1" }, + { "Keypad2", "2" }, + { "Keypad3", "3" }, + { "Keypad4", "4" }, + { "Keypad5", "5" }, + { "Keypad6", "6" }, + { "Keypad7", "7" }, + { "Keypad8", "8" }, + { "Keypad9", "9" }, + { "KeypadDivide", "/" }, + { "KeypadMultiply", "*" }, + { "KeypadMinus", "-" }, + { "KeypadPlus", "+" }, + { "KeypadPeriod", "." }, + { "KeypadEquals", "=" }, + { "KeypadComma", "," }, + { "KeypadLeftParen", "(" }, + { "KeypadRightParen", ")" }, + { "KeypadLeftBrace", "{" }, + { "KeypadRightBrace", "}" }, + { "KeypadTab", "\t" }, + { "KeypadA", "a" }, + { "KeypadB", "b" }, + { "KeypadC", "c" }, + { "KeypadD", "d" }, + { "KeypadE", "e" }, + { "KeypadF", "f" } + }; + + private static readonly Dictionary AllCharactersShiftList = new Dictionary + { + { "A", "A" }, + { "B", "B" }, + { "C", "C" }, + { "D", "D" }, + { "E", "E" }, + { "F", "F" }, + { "G", "G" }, + { "H", "H" }, + { "I", "I" }, + { "J", "J" }, + { "K", "K" }, + { "L", "L" }, + { "M", "M" }, + { "N", "N" }, + { "O", "O" }, + { "P", "P" }, + { "Q", "Q" }, + { "R", "R" }, + { "S", "S" }, + { "T", "T" }, + { "U", "U" }, + { "V", "V" }, + { "W", "W" }, + { "X", "X" }, + { "Y", "Y" }, + { "Z", "Z" }, + { "D1", "!" }, + { "D2", "@" }, + { "D3", "#" }, + { "D4", "$" }, + { "D5", "%" }, + { "D6", "^" }, + { "D7", "&" }, + { "D8", "*" }, + { "D9", "(" }, + { "D0", ")" }, + { "Space", " " }, + { "Minus", "_" }, + { "Equals", "+" }, + { "LeftBracket", "{" }, + { "RightBracket", "}" }, + { "Backslash", "|" }, + { "Semicolon", ":" }, + { "Apostrophe", "\"" }, + { "Tilde", "~" }, + { "Comma", "<" }, + { "Period", ">" }, + { "Slash", "?" }, + { "Keypad0", ")" }, + { "Keypad00", ")" }, + { "Keypad000", ")" }, + { "Keypad1", "!" }, + { "Keypad2", "@" }, + { "Keypad3", "#" }, + { "Keypad4", "$" }, + { "Keypad5", "%" }, + { "Keypad6", "^" }, + { "Keypad7", "&" }, + { "Keypad8", "*" }, + { "Keypad9", "(" }, + { "KeypadDivide", "/" }, + { "KeypadMultiply", "*" }, + { "KeypadMinus", "_" }, + { "KeypadPlus", "+" }, + { "KeypadPeriod", ">" }, + { "KeypadEquals", "+" }, + { "KeypadComma", "<" }, + { "KeypadLeftParen", "(" }, + { "KeypadRightParen", ")" }, + { "KeypadLeftBrace", "{" }, + { "KeypadRightBrace", "}" }, + { "KeypadTab", "\t" }, + { "KeypadA", "A" }, + { "KeypadB", "B" }, + { "KeypadC", "C" }, + { "KeypadD", "D" }, + { "KeypadE", "E" }, + { "KeypadF", "F" } + }; + + public static readonly Dictionary AlphabetValues = new Dictionary + { + { "A", "a" }, + { "B", "b" }, + { "C", "c" }, + { "D", "d" }, + { "E", "e" }, + { "F", "f" }, + { "G", "g" }, + { "H", "h" }, + { "I", "i" }, + { "J", "j" }, + { "K", "k" }, + { "L", "l" }, + { "M", "m" }, + { "N", "n" }, + { "O", "o" }, + { "P", "p" }, + { "Q", "q" }, + { "R", "r" }, + { "S", "s" }, + { "T", "t" }, + { "U", "u" }, + { "V", "v" }, + { "W", "w" }, + { "X", "x" }, + { "Y", "y" }, + { "Z", "z" }, + }; + + public static readonly Dictionary NumberRowValues = new Dictionary + { + { "D1", "1" }, + { "D2", "2" }, + { "D3", "3" }, + { "D4", "4" }, + { "D5", "5" }, + { "D6", "6" }, + { "D7", "7" }, + { "D8", "8" }, + { "D9", "9" }, + { "D0", "0" } + }; + + public static readonly Dictionary AlphabetKeypadValues = new Dictionary + { + { "A", "a" }, + { "B", "b" }, + { "C", "c" }, + { "D", "d" }, + { "E", "e" }, + { "F", "f" }, + { "G", "g" }, + { "H", "h" }, + { "I", "i" }, + { "J", "j" }, + { "K", "k" }, + { "L", "l" }, + { "M", "m" }, + { "N", "n" }, + { "O", "o" }, + { "P", "p" }, + { "Q", "q" }, + { "R", "r" }, + { "S", "s" }, + { "T", "t" }, + { "U", "u" }, + { "V", "v" }, + { "W", "w" }, + { "X", "x" }, + { "Y", "y" }, + { "Z", "z" }, + { "KeypadA", "a" }, + { "KeypadB", "b" }, + { "KeypadC", "c" }, + { "KeypadD", "d" }, + { "KeypadE", "e" }, + { "KeypadF", "f" } + }; + + public static readonly Dictionary NumberWithKeypadValues = new Dictionary + { + { "D1", "1" }, + { "D2", "2" }, + { "D3", "3" }, + { "D4", "4" }, + { "D5", "5" }, + { "D6", "6" }, + { "D7", "7" }, + { "D8", "8" }, + { "D9", "9" }, + { "D0", "0" }, + { "Keypad0", "0" }, + { "Keypad1", "1" }, + { "Keypad2", "2" }, + { "Keypad3", "3" }, + { "Keypad4", "4" }, + { "Keypad5", "5" }, + { "Keypad6", "6" }, + { "Keypad7", "7" }, + { "Keypad8", "8" }, + { "Keypad9", "9" } + }; + + public static readonly Dictionary SpecialCharacterKeyValues = new Dictionary + { + { "Space", " " }, + { "Minus", "-" }, + { "Equals", "=" }, + { "LeftBracket", "[" }, + { "RightBracket", "]" }, + { "Backslash", "\\" }, + { "Semicolon", ";" }, + { "Apostrophe", "'" }, + { "Tilde", "~" }, + { "Comma", "," }, + { "Period", "." }, + { "Slash", "/" }, + }; + + public static KeyboardHandler Instance = new(); + + public static Dictionary GetShiftDict(Dictionary dict) + { + Dictionary shiftDict = new Dictionary(); + foreach (string key in dict.Keys) + { + if (AllCharactersShiftList.ContainsKey(key)) + shiftDict.Add(key, AllCharactersShiftList[key]); + } + return shiftDict; + } + + public static List KeyValuesToList(Dictionary dict) + { + return dict.Values.ToList(); + } + + public static List TrimKeyList(List keysList) + { + List newKeysList = new List(); + foreach (string key in keysList) + { + if (!newKeysList.Contains(key) && key != "" && key != "\t") + newKeysList.Add(key); + } + return newKeysList; + } + + public static string GetKeyName(Keys? key) + { + string? keyString = key.ToString(); + + if (keyString == null) return string.Empty; + + if (Input.Keyboard.Shift) + { + if (AllCharactersShiftList.ContainsKey(keyString)) + return AllCharactersShiftList[keyString]; + else + return string.Empty; + } + else + { + if (AllCharactersList.ContainsKey(keyString)) + return AllCharactersList[keyString]; + else + return string.Empty; + } + } + + public Keys? GetPressedKey() + { + Keys? key = Input.Keyboard.FirstPressed(); + return key; + } +} diff --git a/Source/Scenes/Overworld.cs b/Source/Scenes/Overworld.cs index ad1d3eb1..0f85722a 100644 --- a/Source/Scenes/Overworld.cs +++ b/Source/Scenes/Overworld.cs @@ -1,5 +1,4 @@ using Celeste64.Mod; - namespace Celeste64; public class Overworld : Scene @@ -271,7 +270,10 @@ public void SlideSelectedMod(int dir) public override void Update() { slide += (index - slide) * (1 - MathF.Pow(.001f, Time.Delta)); - wobble += (Controls.Camera.Value - wobble) * (1 - MathF.Pow(.1f, Time.Delta)); + + if (!Paused) + wobble += (Controls.Camera.Value - wobble) * (1 - MathF.Pow(.1f, Time.Delta)); + Calc.Approach(ref cameraCloseUpEase, state == States.Entering ? 1 : 0, Time.Delta); Calc.Approach(ref selectedEase, state != States.Selecting ? 1 : 0, 8 * Time.Delta); @@ -431,6 +433,15 @@ public override void Update() } else if (Paused) { + /* + * Edge case handler for the OSK, since it has a special case for pressing Enter. + * Enter also happens to be a default binding for pausing. I wish I was kidding. + */ + if ((KeyboardHandler.Instance.GetPressedKey() is Keys.Enter or Keys.Enter2 or Keys.KeypadEnter) && pauseMenu?.CurrentMenu is OnScreenKeyboardMenu) + { + return; + } + if (pauseMenu != null) { pauseMenu.Update();