Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 220 additions & 3 deletions Source/diablo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*
* Implementation of the main game initialization functions.
*/
#include <algorithm>
#include <array>
#include <cstdint>
#include <string_view>
Expand Down Expand Up @@ -175,6 +176,56 @@ bool was_archives_init = false;
/** To know if surfaces have been initialized or not */
bool was_window_init = false;
bool was_ui_init = false;
uint32_t autoSaveNextTimerDueAt = 0;
AutoSaveReason pendingAutoSaveReason = AutoSaveReason::None;
/** Prevent autosave from running immediately after session start before player interaction. */
bool hasEnteredActiveGameplay = false;
uint32_t autoSaveCooldownUntil = 0;
uint32_t autoSaveCombatCooldownUntil = 0;
constexpr uint32_t AutoSaveCooldownMilliseconds = 5000;
constexpr uint32_t AutoSaveCombatCooldownMilliseconds = 4000;
constexpr int AutoSaveEnemyProximityTiles = 6;

uint32_t GetAutoSaveIntervalMilliseconds()
{
return static_cast<uint32_t>(std::max(1, *GetOptions().Gameplay.autoSaveIntervalSeconds)) * 1000;
}

int GetAutoSavePriority(AutoSaveReason reason)
{
switch (reason) {
case AutoSaveReason::BossKill:
return 4;
case AutoSaveReason::TownEntry:
return 3;
case AutoSaveReason::UniquePickup:
return 2;
case AutoSaveReason::Timer:
return 1;
case AutoSaveReason::None:
return 0;
}

return 0;
}

const char *GetAutoSaveReasonName(AutoSaveReason reason)
{
switch (reason) {
case AutoSaveReason::None:
return "None";
case AutoSaveReason::Timer:
return "Timer";
case AutoSaveReason::TownEntry:
return "TownEntry";
case AutoSaveReason::BossKill:
return "BossKill";
case AutoSaveReason::UniquePickup:
return "UniquePickup";
}

return "Unknown";
}

void StartGame(interface_mode uMsg)
{
Expand All @@ -194,6 +245,11 @@ void StartGame(interface_mode uMsg)
sgnTimeoutCurs = CURSOR_NONE;
sgbMouseDown = CLICK_NONE;
LastPlayerAction = PlayerActionType::None;
hasEnteredActiveGameplay = false;
autoSaveCooldownUntil = 0;
autoSaveCombatCooldownUntil = 0;
pendingAutoSaveReason = AutoSaveReason::None;
autoSaveNextTimerDueAt = SDL_GetTicks() + GetAutoSaveIntervalMilliseconds();
}

void FreeGame()
Expand Down Expand Up @@ -775,9 +831,11 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState)
ReleaseKey(SDLC_EventKey(event));
return;
case SDL_EVENT_MOUSE_MOTION:
if (ControlMode == ControlTypes::KeyboardAndMouse && invflag)
InvalidateInventorySlot();
MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) };
if (ControlMode == ControlTypes::KeyboardAndMouse) {
if (invflag)
InvalidateInventorySlot();
MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) };
}
gmenu_on_mouse_move();
return;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
Expand Down Expand Up @@ -1553,6 +1611,26 @@ void GameLogic()
RedrawViewport();
pfile_update(false);

if (!hasEnteredActiveGameplay && LastPlayerAction != PlayerActionType::None)
hasEnteredActiveGameplay = true;

if (*GetOptions().Gameplay.autoSaveEnabled) {
const uint32_t now = SDL_GetTicks();
if (SDL_TICKS_PASSED(now, autoSaveNextTimerDueAt)) {
QueueAutoSave(AutoSaveReason::Timer);
}
} else {
autoSaveNextTimerDueAt = SDL_GetTicks() + GetAutoSaveIntervalMilliseconds();
pendingAutoSaveReason = AutoSaveReason::None;
}

if (HasPendingAutoSave() && IsAutoSaveSafe()) {
if (AttemptAutoSave(pendingAutoSaveReason)) {
pendingAutoSaveReason = AutoSaveReason::None;
autoSaveNextTimerDueAt = SDL_GetTicks() + GetAutoSaveIntervalMilliseconds();
}
}

plrctrls_after_game_logic();
}

Expand Down Expand Up @@ -1802,6 +1880,143 @@ const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueCha

} // namespace

bool IsEnemyTooCloseForAutoSave();

bool IsAutoSaveSafe()
{
if (gbIsMultiplayer || !gbRunGame)
return false;

if (!hasEnteredActiveGameplay)
return false;

if (!SDL_TICKS_PASSED(SDL_GetTicks(), autoSaveCooldownUntil))
return false;

if (!SDL_TICKS_PASSED(SDL_GetTicks(), autoSaveCombatCooldownUntil))
return false;

if (movie_playing || PauseMode != 0 || gmenu_is_active() || IsPlayerInStore())
return false;

if (MyPlayer == nullptr || IsPlayerDead() || MyPlayer->_pLvlChanging || LoadingMapObjects)
return false;

if (qtextflag || DropGoldFlag || IsWithdrawGoldOpen || pcurs != CURSOR_HAND)
return false;

if (leveltype != DTYPE_TOWN && IsEnemyTooCloseForAutoSave())
return false;

return true;
}

void MarkCombatActivity()
{
autoSaveCombatCooldownUntil = SDL_GetTicks() + AutoSaveCombatCooldownMilliseconds;
}

bool IsEnemyTooCloseForAutoSave()
{
if (MyPlayer == nullptr)
return false;

const Point playerPosition = MyPlayer->position.tile;
for (size_t i = 0; i < ActiveMonsterCount; i++) {
const Monster &monster = Monsters[ActiveMonsters[i]];
if (monster.hitPoints <= 0 || monster.mode == MonsterMode::Death || monster.mode == MonsterMode::Petrified)
continue;

if (monster.type().type == MT_GOLEM)
continue;

if ((monster.flags & MFLAG_HIDDEN) != 0)
continue;

const int distance = std::max(
std::abs(monster.position.tile.x - playerPosition.x),
std::abs(monster.position.tile.y - playerPosition.y));
if (distance <= AutoSaveEnemyProximityTiles)
return true;
}

return false;
}

int GetSecondsUntilNextAutoSave()
{
if (!*GetOptions().Gameplay.autoSaveEnabled)
return -1;

if (IsAutoSavePending())
return 0;

const uint32_t now = SDL_GetTicks();
if (SDL_TICKS_PASSED(now, autoSaveNextTimerDueAt))
return 0;

const uint32_t remainingMilliseconds = autoSaveNextTimerDueAt - now;
return static_cast<int>((remainingMilliseconds + 999) / 1000);
}

bool HasPendingAutoSave()
{
return pendingAutoSaveReason != AutoSaveReason::None;
}

void RequestAutoSave(AutoSaveReason reason)
{
if (!*GetOptions().Gameplay.autoSaveEnabled)
return;

if (gbIsMultiplayer)
return;

QueueAutoSave(reason);
}

bool IsAutoSavePending()
{
return HasPendingAutoSave();
}

void QueueAutoSave(AutoSaveReason reason)
{
if (gbIsMultiplayer)
return;

if (!*GetOptions().Gameplay.autoSaveEnabled)
return;

if (GetAutoSavePriority(reason) > GetAutoSavePriority(pendingAutoSaveReason)) {
pendingAutoSaveReason = reason;
LogVerbose("Autosave queued: {}", GetAutoSaveReasonName(reason));
}
}

bool AttemptAutoSave(AutoSaveReason reason)
{
if (!IsAutoSaveSafe())
return false;

const EventHandler saveProc = SetEventHandler(DisableInputEventHandler);
const uint32_t currentTime = SDL_GetTicks();
SaveGame();
const uint32_t afterSaveTime = SDL_GetTicks();

autoSaveCooldownUntil = afterSaveTime + AutoSaveCooldownMilliseconds;
if (gbValidSaveFile) {
autoSaveNextTimerDueAt = afterSaveTime + GetAutoSaveIntervalMilliseconds();
if (reason != AutoSaveReason::Timer) {
const int timeElapsed = static_cast<int>(afterSaveTime - currentTime);
const int displayTime = std::max(500, 1000 - timeElapsed);
InitDiabloMsg(EMSG_GAME_SAVED, displayTime);
}
}
SetEventHandler(saveProc);
return gbValidSaveFile;
}

void InitKeymapActions()
{
Options &options = GetOptions();
Expand Down Expand Up @@ -3434,6 +3649,8 @@ tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir)
CompleteProgress();

LoadGameLevelCalculateCursor();
if (leveltype == DTYPE_TOWN && lvldir != ENTRY_LOAD && !firstflag)
::devilution::RequestAutoSave(AutoSaveReason::TownEntry);
return {};
}

Expand Down
16 changes: 16 additions & 0 deletions Source/diablo.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ enum class PlayerActionType : uint8_t {
OperateObject,
};

enum class AutoSaveReason : uint8_t {
None,
Timer,
TownEntry,
BossKill,
UniquePickup,
};

extern uint32_t DungeonSeeds[NUMLEVELS];
extern DVL_API_FOR_TEST std::optional<uint32_t> LevelSeeds[NUMLEVELS];
extern Point MousePosition;
Expand Down Expand Up @@ -101,6 +109,14 @@ bool PressEscKey();
void DisableInputEventHandler(const SDL_Event &event, uint16_t modState);
tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir);
bool IsDiabloAlive(bool playSFX);
void MarkCombatActivity();
bool IsAutoSaveSafe();
int GetSecondsUntilNextAutoSave();
bool HasPendingAutoSave();
bool IsAutoSavePending();
void RequestAutoSave(AutoSaveReason reason);
void QueueAutoSave(AutoSaveReason reason);
bool AttemptAutoSave(AutoSaveReason reason);
void PrintScreen(SDL_Keycode vkey);

/**
Expand Down
31 changes: 31 additions & 0 deletions Source/gamemenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
*/
#include "gamemenu.h"

#include <fmt/format.h>
#include <string>

#ifdef USE_SDL3
#include <SDL3/SDL_timer.h>
#endif

#include "cursor.h"
#include "diablo.h"
#include "diablo_msg.hpp"
#include "engine/backbuffer_state.hpp"
#include "engine/demomode.h"
Expand Down Expand Up @@ -89,6 +93,8 @@ const char *const SoundToggleNames[] = {
N_("Sound Disabled"),
};

std::string saveGameMenuLabel;

void GamemenuUpdateSingle()
{
sgSingleMenu[2].setEnabled(gbValidSaveFile);
Expand All @@ -98,6 +104,23 @@ void GamemenuUpdateSingle()
sgSingleMenu[0].setEnabled(enable);
}

std::string_view GetSaveGameMenuLabel()
{
if (HasPendingAutoSave()) {
saveGameMenuLabel = fmt::format(fmt::runtime(_("Save Game ({:s})")), _("ready"));
return saveGameMenuLabel;
}

const int seconds = GetSecondsUntilNextAutoSave();
if (seconds < 0) {
saveGameMenuLabel = _("Save Game");
return saveGameMenuLabel;
}

saveGameMenuLabel = fmt::format(fmt::runtime(_("Save Game ({:d})")), seconds);
return saveGameMenuLabel;
}

void GamemenuPrevious(bool /*bActivate*/)
{
gamemenu_on();
Expand Down Expand Up @@ -363,6 +386,14 @@ void gamemenu_save_game(bool /*bActivate*/)
SetEventHandler(saveProc);
}

std::string_view GetGamemenuText(const TMenuItem &menuItem)
{
if (menuItem.fnMenu == &gamemenu_save_game)
return GetSaveGameMenuLabel();

return _(menuItem.pszStr);
}

void gamemenu_on()
{
isGameMenuOpen = true;
Expand Down
5 changes: 5 additions & 0 deletions Source/gamemenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
*/
#pragma once

#include <string_view>

namespace devilution {

struct TMenuItem;

void gamemenu_on();
void gamemenu_off();
void gamemenu_handle_previous();
void gamemenu_exit_game(bool bActivate);
void gamemenu_quit_game(bool bActivate);
void gamemenu_load_game(bool bActivate);
void gamemenu_save_game(bool bActivate);
std::string_view GetGamemenuText(const TMenuItem &menuItem);

extern bool isGameMenuOpen;

Expand Down
Loading