diff --git a/simulation_parameters/Constants.cs b/simulation_parameters/Constants.cs index 4de88946615..317ff16873f 100644 --- a/simulation_parameters/Constants.cs +++ b/simulation_parameters/Constants.cs @@ -1186,6 +1186,8 @@ public static class Constants /// public const float MICROBE_MIN_ABSORB_RADIUS = 3; + public const float HARDCORE_AUTOSAVE_INTERVAL = 60; + public const float PROCEDURAL_CACHE_CLEAN_INTERVAL = 9.3f; public const float PROCEDURAL_CACHE_MEMBRANE_KEEP_TIME = 500; public const float PROCEDURAL_CACHE_MICROBE_SHAPE_TIME = 7000; diff --git a/src/general/NewGameSettings.cs b/src/general/NewGameSettings.cs index c22103affc2..7f6e7b12b74 100644 --- a/src/general/NewGameSettings.cs +++ b/src/general/NewGameSettings.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Godot; using Xoshiro.PRNG64; using Container = Godot.Container; @@ -211,6 +212,12 @@ public partial class NewGameSettings : ControlWithInput private Button includeMulticellularButton = null!; private Button easterEggsButton = null!; + [Export] + private Button hardcoreModeButton = null!; + + [Export] + private LineEdit hardcoreModeName = null!; + // Other private Container checkOptionsMenuAdviceContainer = null!; #pragma warning restore CA2213 @@ -228,6 +235,10 @@ public partial class NewGameSettings : ControlWithInput private DifficultyPreset normal = null!; private DifficultyPreset custom = null!; + private Task>? saveListTask; + private bool isListingSaves; + private bool listedSavesDone; + [Signal] public delegate void OnNewGameSettingsClosedEventHandler(); @@ -348,6 +359,37 @@ public override void _Ready() } } + public override void _Process(double delta) + { + if (!isListingSaves || saveListTask == null) + return; + + if (saveListTask.IsCanceled) + { + saveListTask.Dispose(); + saveListTask = null; + + isListingSaves = false; + listedSavesDone = false; + + startButton.Disabled = false; + + return; + } + + if (saveListTask.IsCompleted) + { + // saveListTask disposing is handled further down in the code + isListingSaves = false; + listedSavesDone = true; + + startButton.Disabled = false; + + // Re-do the start button pressed action as it is what started the task + OnConfirmPressed(); + } + } + [RunOnKeyDown("ui_cancel", Priority = Constants.SUBMENU_CANCEL_PRIORITY)] public bool OnEscapePressed() { @@ -424,6 +466,7 @@ public void OpenFromDescendScreen(GameProperties currentGame) lawkButton.ButtonPressed = false; easterEggsButton.ButtonPressed = settings.EasterEggs; + hardcoreModeButton.ButtonPressed = settings.HardcoreMode; } public void ReportValidityOfGameSeed(bool valid) @@ -497,6 +540,12 @@ protected override void Dispose(bool disposing) StartButtonPath.Dispose(); CheckOptionsMenuAdviceContainerPath.Dispose(); } + + if (saveListTask != null) + { + saveListTask.Dispose(); + saveListTask = null; + } } base.Dispose(disposing); @@ -608,6 +657,12 @@ private void StartGame() settings.IncludeMulticellular = includeMulticellularButton.ButtonPressed; settings.EasterEggs = easterEggsButton.ButtonPressed; + settings.HardcoreMode = hardcoreModeButton.ButtonPressed; + + if (settings.HardcoreMode) + settings.HardcoreModeName = hardcoreModeName.Text; + else + settings.HardcoreModeName = null; // Stop music for the video (stop is used instead of pause to stop the menu music playing a bit after the video // before the stage music starts) @@ -691,6 +746,52 @@ private void SetAdvancedView(bool advanced) private void OnConfirmPressed() { + if (hardcoreModeButton.ButtonPressed) + { + // Hardcore mode name must not be null or empty + if (string.IsNullOrEmpty(hardcoreModeName.Text)) + return; + + // Check if there isn't any saves with same name as new hardcore mode name + if (!isListingSaves) + { + if (listedSavesDone) + { + isListingSaves = false; + listedSavesDone = false; + var saveList = saveListTask!.Result; + + saveListTask.Dispose(); + saveListTask = null; + + // Final check + foreach (var saveName in saveList) + { + if (saveName.GetBaseName().GetFile() == hardcoreModeName.Text) + { + return; + } + } + } + else + { + isListingSaves = true; + + saveListTask = new Task>(() => SaveHelper.CreateListOfSaves()); + TaskExecutor.Instance.AddTask(saveListTask); + + // Greys out start button for more information + startButton.Disabled = true; + + return; + } + } + else + { + return; + } + } + GUICommon.Instance.PlayButtonPressSound(); StartGame(); @@ -968,6 +1069,11 @@ private void OnEasterEggsToggled(bool pressed) _ = pressed; } + private void OnHardcoreModeToggled(bool pressed) + { + hardcoreModeName.Editable = pressed; + } + private void PerformanceNoteLinkClicked(Variant meta) { if (meta.VariantType != Variant.Type.String) diff --git a/src/general/NewGameSettings.tscn b/src/general/NewGameSettings.tscn index 86488d3b958..29ee2616d0d 100644 --- a/src/general/NewGameSettings.tscn +++ b/src/general/NewGameSettings.tscn @@ -27,7 +27,7 @@ font = ExtResource("5_otvf3") [sub_resource type="StyleBoxEmpty" id="4"] -[node name="NewGameSettings" type="Control"] +[node name="NewGameSettings" type="Control" node_paths=PackedStringArray("hardcoreModeButton", "hardcoreModeName")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -82,6 +82,8 @@ IncludeMulticellularButtonPath = NodePath("CenterContainer/VBoxContainer/Advance EasterEggsButtonPath = NodePath("CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer3/EasterEggs") StartButtonPath = NodePath("CenterContainer/VBoxContainer/HBoxContainer/Start") CheckOptionsMenuAdviceContainerPath = NodePath("CenterContainer/VBoxContainer/BasicOptions/Main/VBoxContainer/CheckPerformanceSettingsContainer") +hardcoreModeButton = NodePath("CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4/HardcoreMode") +hardcoreModeName = NodePath("CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4/HardcoreGameName") [node name="CenterContainer" type="CenterContainer" parent="."] layout_mode = 0 @@ -910,6 +912,29 @@ text = "EASTER_EGGS_EXPLANATION" label_settings = ExtResource("5_rnvau") autowrap_mode = 3 +[node name="VBoxContainer4" type="VBoxContainer" parent="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer"] +layout_mode = 2 + +[node name="HardcoreMode" type="CheckBox" parent="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "HARDCORE_MODE" + +[node name="Label" type="Label" parent="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_horizontal = 3 +text = "HARDCORE_MODE_EXPLANATION" +label_settings = ExtResource("5_rnvau") +autowrap_mode = 3 + +[node name="HardcoreGameName" type="LineEdit" parent="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4"] +editor_description = "PLACEHOLDER" +custom_minimum_size = Vector2(175, 0) +layout_mode = 2 +tooltip_text = "RANDOM_SEED_TOOLTIP" +editable = false + [node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] layout_mode = 2 @@ -986,6 +1011,7 @@ text = "START_GAME" [connection signal="pressed" from="CenterContainer/VBoxContainer/AdvancedOptions/Planet/VBoxContainer/HBoxContainer3/HBoxContainer/RandomizeButtonAdvanced" to="." method="OnRandomisedGameSeedPressed"] [connection signal="toggled" from="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer2/IncludeMulticellular" to="." method="OnIncludeMulticellularToggled"] [connection signal="toggled" from="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer3/EasterEggs" to="." method="OnEasterEggsToggled"] +[connection signal="toggled" from="CenterContainer/VBoxContainer/AdvancedOptions/Misc/VBoxContainer/VBoxContainer4/HardcoreMode" to="." method="OnHardcoreModeToggled"] [connection signal="pressed" from="CenterContainer/VBoxContainer/HBoxContainer/Back" to="." method="OnBackPressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/HBoxContainer/Basic" to="." method="OnBasicPressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/HBoxContainer/Advanced" to="." method="OnAdvancedPressed"] diff --git a/src/general/PauseMenu.cs b/src/general/PauseMenu.cs index 20838eaec57..25f3bc7d02e 100644 --- a/src/general/PauseMenu.cs +++ b/src/general/PauseMenu.cs @@ -501,6 +501,14 @@ private void OpenLoadPressed() { GUICommon.Instance.PlayButtonPressSound(); + if (GameProperties != null) + { + if (GameProperties.GameWorld.WorldSettings.HardcoreMode) + { + return; + } + } + ActiveMenu = ActiveMenuType.Load; } @@ -545,6 +553,14 @@ private void OpenSavePressed() { GUICommon.Instance.PlayButtonPressSound(); + if (GameProperties != null) + { + if (GameProperties.GameWorld.WorldSettings.HardcoreMode) + { + return; + } + } + ActiveMenu = ActiveMenuType.Save; } diff --git a/src/general/WorldGenerationSettings.cs b/src/general/WorldGenerationSettings.cs index 3527fe4ecab..ec9b1906393 100644 --- a/src/general/WorldGenerationSettings.cs +++ b/src/general/WorldGenerationSettings.cs @@ -126,6 +126,16 @@ public enum LifeOrigin /// public bool EasterEggs { get; set; } = true; + /// + /// This thing right here... It is... unforgiving. + /// + public bool HardcoreMode { get; set; } + + /// + /// Unchangeable name for hardcore mode save + /// + public string? HardcoreModeName { get; set; } + /// /// The auto-evo configuration this world uses /// @@ -186,6 +196,7 @@ public override string ToString() $", Day length: {DayLength}" + $", Include multicellular: {IncludeMulticellular}" + $", Easter eggs: {EasterEggs}" + + $", Hardcore mode: {HardcoreMode}" + "]"; } } diff --git a/src/general/base_stage/EditorBase.cs b/src/general/base_stage/EditorBase.cs index 5200d32a208..110a8411b8b 100644 --- a/src/general/base_stage/EditorBase.cs +++ b/src/general/base_stage/EditorBase.cs @@ -789,6 +789,11 @@ protected virtual void PerformQuickSave() throw new GodotAbstractMethodNotOverriddenException(); } + protected virtual void PerformHardcoreModeSave() + { + throw new GodotAbstractMethodNotOverriddenException(); + } + protected virtual void SaveGame(string name) { throw new GodotAbstractMethodNotOverriddenException(); diff --git a/src/general/base_stage/StageBase.cs b/src/general/base_stage/StageBase.cs index 52287dc2561..6a95f2fdbd2 100644 --- a/src/general/base_stage/StageBase.cs +++ b/src/general/base_stage/StageBase.cs @@ -44,6 +44,11 @@ public partial class StageBase : NodeWithInput, IStageBase, IGodotEarlyNodeResol /// private double elapsedSinceLightLevelUpdate = 1; + /// + /// Hardcore mode auto-saves during gameplay regularly. + /// + private double hardcoreModeAutosaveTimer = 2; + protected StageBase() { } @@ -127,6 +132,19 @@ public override void _Process(double delta) wantsToSave = false; } + if (CurrentGame != null) + { + if (CurrentGame.GameWorld.WorldSettings.HardcoreMode) + { + hardcoreModeAutosaveTimer -= delta; + if (hardcoreModeAutosaveTimer <= 0) + { + hardcoreModeAutosaveTimer = Constants.HARDCORE_AUTOSAVE_INTERVAL; + AutoSave(); + } + } + } + GameWorld.Process((float)delta); elapsedSinceLightLevelUpdate += delta; @@ -271,6 +289,11 @@ protected virtual void PerformQuickSave() throw new GodotAbstractMethodNotOverriddenException(); } + protected virtual void PerformHardcoreModeSave() + { + throw new GodotAbstractMethodNotOverriddenException(); + } + protected virtual void OnLightLevelUpdate() { throw new GodotAbstractMethodNotOverriddenException(); diff --git a/src/microbe_stage/MicrobeStage.cs b/src/microbe_stage/MicrobeStage.cs index 7ae42305ef8..82fa02621d3 100644 --- a/src/microbe_stage/MicrobeStage.cs +++ b/src/microbe_stage/MicrobeStage.cs @@ -175,6 +175,34 @@ public override void ResolveNodeReferences() guidanceLine = GetNode(GuidanceLinePath); } + public override void _Notification(int notification) + { + base._Notification(notification); + + if (notification == NotificationWMCloseRequest) + { + if (!GameWorld.WorldSettings.HardcoreMode) + { + GD.Print("Closing game directly from Microbe Stage"); + SceneManager.Instance.QuitThrive(); + return; + } + + if (SaveHelper.CurrentSaveAction == null) + return; + + if (SaveHelper.CurrentSaveAction.Method.IsFinal) + { + GD.Print("Closing game after hardcore mode save"); + SceneManager.Instance.QuitThrive(); + } + else + { + GD.Print("Not closing game due to hardcore mode save not being completed"); + } + } + } + public override void _EnterTree() { base._EnterTree(); @@ -185,6 +213,9 @@ public override void _EnterTree() public override void _ExitTree() { + if (WorldSettings.HardcoreMode) + SaveHelper.HardcoreModeSave(WorldSettings.HardcoreModeName!, this, true); + base._ExitTree(); CheatManager.OnSpawnEnemyCheatUsed -= OnSpawnEnemyCheatUsed; CheatManager.OnPlayerDuplicationCheatUsed -= OnDuplicatePlayerCheatUsed; @@ -925,14 +956,34 @@ protected override void PlayerExtinctInPatch() protected override void AutoSave() { + if (WorldSettings.HardcoreMode) + { + PerformHardcoreModeSave(); + + return; + } + SaveHelper.AutoSave(this); } protected override void PerformQuickSave() { + if (WorldSettings.HardcoreMode) + { + return; + } + SaveHelper.QuickSave(this); } + protected override void PerformHardcoreModeSave() + { + if (!WorldSettings.HardcoreMode) + return; + + SaveHelper.HardcoreModeSave(WorldSettings.HardcoreModeName!, this); + } + protected override void UpdatePatchSettings(bool promptPatchNameChange = true) { // TODO: would be nice to skip this if we are loading a save made in the editor as this gets called twice when @@ -1124,6 +1175,9 @@ private void OnPlayerDied(Entity player) if (!engulfed) TutorialState.SendEvent(TutorialEventType.MicrobePlayerDied, EventArgs.Empty, this); + if (CurrentGame!.GameWorld.WorldSettings.HardcoreMode) + AutoSave(); + // Don't clear the player object here as we want to wait until the player entity is deleted before creating // a new one to avoid having two player entities existing at the same time } diff --git a/src/microbe_stage/editor/MicrobeEditor.cs b/src/microbe_stage/editor/MicrobeEditor.cs index 6b549ce8282..c2c4055486b 100644 --- a/src/microbe_stage/editor/MicrobeEditor.cs +++ b/src/microbe_stage/editor/MicrobeEditor.cs @@ -261,14 +261,34 @@ protected override void OnRedoPerformed() protected override void PerformAutoSave() { + if (CurrentGame.GameWorld.WorldSettings.HardcoreMode) + { + PerformHardcoreModeSave(); + + return; + } + SaveHelper.AutoSave(this); } protected override void PerformQuickSave() { + if (CurrentGame.GameWorld.WorldSettings.HardcoreMode) + { + return; + } + SaveHelper.QuickSave(this); } + protected override void PerformHardcoreModeSave() + { + if (!CurrentGame.GameWorld.WorldSettings.HardcoreMode) + return; + + SaveHelper.HardcoreModeSave(CurrentGame.GameWorld.WorldSettings.HardcoreModeName!, this); + } + protected override void SaveGame(string name) { SaveHelper.Save(name, this); diff --git a/src/saving/InProgressSave.cs b/src/saving/InProgressSave.cs index 4eb2d87bec0..4dde821d269 100644 --- a/src/saving/InProgressSave.cs +++ b/src/saving/InProgressSave.cs @@ -42,6 +42,8 @@ public class InProgressSave : IDisposable private bool wasColourblindScreenFilterVisible; + private bool pauseCompletely; + public InProgressSave(SaveInformation.SaveType type, Func currentGameRoot, Func createSaveData, Action performSave, string? saveName) { @@ -79,11 +81,12 @@ private enum State public SaveInformation.SaveType Type { get; } - public void Start() + public void Start(bool pause = false) { PauseManager.Instance.AddPause(nameof(InProgressSave)); IsSaving = true; + pauseCompletely = pause; Invoke.Instance.Perform(Step); } @@ -169,6 +172,9 @@ private static int FindExistingSavesOfType(out int totalCount, out string? oldes private void Step() { + if (pauseCompletely) + PauseManager.Instance.AddPause(nameof(InProgressSave)); + switch (state) { case State.Initial: diff --git a/src/saving/SaveHelper.cs b/src/saving/SaveHelper.cs index 47811c2851d..c932e77c3ec 100644 --- a/src/saving/SaveHelper.cs +++ b/src/saving/SaveHelper.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Godot; +using JetBrains.Annotations; using DirAccess = Godot.DirAccess; using FileAccess = Godot.FileAccess; using Path = System.IO.Path; @@ -15,6 +16,8 @@ /// public static class SaveHelper { + public static Action? CurrentSaveAction; + /// /// This is a list of known versions where save compatibility is very broken and loading needs to be prevented /// (unless there exists a version converter) @@ -220,6 +223,39 @@ public static QuickLoadError QuickLoad() return QuickLoadError.None; } + /// + /// Save the game into the main save (for hardcore mode) + /// + public static void HardcoreModeSave(string name, MicrobeStage state, bool instant = false) + { + if (instant) + { + new Save + { + SavedProperties = state.CurrentGame, + MicrobeStage = state, + }.SaveToFile(); + + return; + } + + InternalSaveHelper(SaveInformation.SaveType.Manual, MainGameState.MicrobeStage, save => + { + save.SavedProperties = state.CurrentGame; + save.MicrobeStage = state; + save.SaveToFile(); + }, () => state, name, true); + } + + public static void HardcoreModeSave(string name, MicrobeEditor state, bool instant = false) + { + InternalSaveHelper(SaveInformation.SaveType.Manual, MainGameState.MicrobeEditor, save => + { + save.SavedProperties = state.CurrentGame; + save.MicrobeEditor = state; + }, () => state, name, true); + } + /// /// Returns a list of all saves /// @@ -460,7 +496,7 @@ public static void ClearLastSaveTime() } private static void InternalSaveHelper(SaveInformation.SaveType type, MainGameState gameState, - Action copyInfoToSave, Func stateRoot, string? saveName = null) + Action copyInfoToSave, Func stateRoot, string? saveName = null, bool instant = false) { if (type == SaveInformation.SaveType.QuickSave && !AllowQuickSavingAndLoading) { @@ -474,21 +510,22 @@ private static void InternalSaveHelper(SaveInformation.SaveType type, MainGameSt return; } - new InProgressSave(type, stateRoot, data => - CreateSaveObject(gameState, data.Type), - (inProgress, save) => - { - copyInfoToSave.Invoke(save); + CurrentSaveAction = (inProgress, save) => + { + copyInfoToSave.Invoke(save); - if (PreventSavingIfExtinct(inProgress, save)) - return; + if (PreventSavingIfExtinct(inProgress, save)) + return; - if (PreventSavingIfInPrototype(inProgress, save)) - return; + if (PreventSavingIfInPrototype(inProgress, save)) + return; - PerformSave(inProgress, save); - }, saveName).Start(); - } + PerformSave(inProgress, save); + }; + + new InProgressSave(type, stateRoot, data => + CreateSaveObject(gameState, data.Type), CurrentSaveAction, saveName).Start(instant); + } private static Save CreateSaveObject(MainGameState gameState, SaveInformation.SaveType type) {