Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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 All @@ -19,7 +20,7 @@
#endif
#endif

#include <fmt/format.h>

Check warning on line 23 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:23:1 [misc-include-cleaner]

included header format.h is not used directly

#include <config.h>

Expand All @@ -42,7 +43,7 @@
#include "diablo_msg.hpp"
#include "discord/discord.h"
#include "doom.h"
#include "encrypt.h"

Check warning on line 46 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:46:1 [misc-include-cleaner]

included header encrypt.h is not used directly
#include "engine/backbuffer_state.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/demomode.h"
Expand All @@ -61,10 +62,10 @@
#include "hwcursor.hpp"
#include "init.hpp"
#include "inv.h"
#include "levels/drlg_l1.h"

Check warning on line 65 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:65:1 [misc-include-cleaner]

included header drlg_l1.h is not used directly
#include "levels/drlg_l2.h"

Check warning on line 66 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:66:1 [misc-include-cleaner]

included header drlg_l2.h is not used directly
#include "levels/drlg_l3.h"

Check warning on line 67 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:67:1 [misc-include-cleaner]

included header drlg_l3.h is not used directly
#include "levels/drlg_l4.h"

Check warning on line 68 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:68:1 [misc-include-cleaner]

included header drlg_l4.h is not used directly
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "levels/themes.h"
Expand Down Expand Up @@ -112,10 +113,10 @@
#include "utils/paths.h"
#include "utils/screen_reader.hpp"
#include "utils/sdl_compat.h"
#include "utils/sdl_thread.h"

Check warning on line 116 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:116:1 [misc-include-cleaner]

included header sdl_thread.h is not used directly
#include "utils/status_macros.hpp"
#include "utils/str_cat.hpp"
#include "utils/utf8.hpp"

Check warning on line 119 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:119:1 [misc-include-cleaner]

included header utf8.hpp is not used directly

#ifndef USE_SDL1
#include "controls/touch/gamepad.h"
Expand All @@ -133,8 +134,8 @@
namespace devilution {

uint32_t DungeonSeeds[NUMLEVELS];
std::optional<uint32_t> LevelSeeds[NUMLEVELS];

Check warning on line 137 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:137:6 [misc-include-cleaner]

no header providing "std::optional" is directly included
Point MousePosition;

Check warning on line 138 in Source/diablo.cpp

View workflow job for this annotation

GitHub Actions / tidy-check

Source/diablo.cpp:138:1 [misc-include-cleaner]

no header providing "devilution::Point" is directly included
bool gbRunGameResult;
bool ReturnToMainMenu;
/** Enable updating of player character, set to false once Diablo dies */
Expand Down Expand Up @@ -175,6 +176,56 @@
/** 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 @@
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 @@
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 @@
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 @@

} // 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 @@
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
35 changes: 35 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,27 @@ void GamemenuUpdateSingle()
sgSingleMenu[0].setEnabled(enable);
}

std::string_view GetSaveGameMenuLabel()
{
#ifndef _DEBUG
return _("Save Game");
#else
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;
#endif
}

void GamemenuPrevious(bool /*bActivate*/)
{
gamemenu_on();
Expand Down Expand Up @@ -363,6 +390,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
Loading