Browse Source

Merge c1a6b61cdc into 5a08031caf

pull/8497/merge
morfidon 5 days ago committed by GitHub
parent
commit
08790ff340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 222
      Source/diablo.cpp
  2. 15
      Source/diablo.h
  3. 58
      Source/gamemenu.cpp
  4. 5
      Source/gamemenu.h
  5. 6
      Source/gmenu.cpp
  6. 8
      Source/inv.cpp
  7. 71
      Source/loadsave.cpp
  8. 14
      Source/loadsave.h
  9. 2
      Source/monster.cpp
  10. 10
      Source/options.cpp
  11. 4
      Source/options.h
  12. 187
      Source/pfile.cpp
  13. 4
      Source/pfile.h
  14. 25
      Source/player.cpp
  15. 4
      Source/utils/sdl2_backports.h
  16. 1
      Source/utils/sdl2_to_1_2_backports.h

222
Source/diablo.cpp

@ -3,6 +3,7 @@
*
* Implementation of the main game initialization functions.
*/
#include <algorithm>
#include <array>
#include <cstdint>
#include <string_view>
@ -175,6 +176,72 @@ bool was_archives_init = false;
/** To know if surfaces have been initialized or not */
bool was_window_init = false;
bool was_ui_init = false;
using TickCount = decltype(SDL_GetTicks());
TickCount autoSaveNextTimerDueAt = 0;
AutoSaveReason pendingAutoSaveReason = AutoSaveReason::None;
/** Prevent autosave from running immediately after session start before player interaction. */
bool hasEnteredActiveGameplay = false;
TickCount autoSaveCooldownUntil = 0;
TickCount autoSaveCombatCooldownUntil = 0;
bool wasAutoSaveEnabled = false;
constexpr TickCount AutoSaveCooldownMilliseconds = 5000;
constexpr TickCount AutoSaveCombatCooldownMilliseconds = 4000;
bool HaveTicksPassed(TickCount now, TickCount due)
{
#ifdef USE_SDL3
return now >= due;
#else
return SDL_TICKS_PASSED(now, due);
#endif
}
bool HaveTicksPassed(TickCount due)
{
return HaveTicksPassed(SDL_GetTicks(), due);
}
TickCount GetAutoSaveIntervalMilliseconds()
{
return static_cast<TickCount>(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)
{
@ -194,6 +261,12 @@ 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();
wasAutoSaveEnabled = *GetOptions().Gameplay.autoSaveEnabled;
}
void FreeGame()
@ -1553,6 +1626,32 @@ void GameLogic()
RedrawViewport();
pfile_update(false);
if (!hasEnteredActiveGameplay && LastPlayerAction != PlayerActionType::None)
hasEnteredActiveGameplay = true;
const bool autoSaveEnabled = *GetOptions().Gameplay.autoSaveEnabled;
if (autoSaveEnabled != wasAutoSaveEnabled) {
if (!autoSaveEnabled)
pendingAutoSaveReason = AutoSaveReason::None;
autoSaveNextTimerDueAt = SDL_GetTicks() + GetAutoSaveIntervalMilliseconds();
wasAutoSaveEnabled = autoSaveEnabled;
}
if (autoSaveEnabled) {
const TickCount now = SDL_GetTicks();
if (HaveTicksPassed(now, autoSaveNextTimerDueAt)) {
QueueAutoSave(AutoSaveReason::Timer);
}
}
if (HasPendingAutoSave() && IsAutoSaveSafe()) {
if (AttemptAutoSave(pendingAutoSaveReason)) {
pendingAutoSaveReason = AutoSaveReason::None;
autoSaveNextTimerDueAt = SDL_GetTicks() + GetAutoSaveIntervalMilliseconds();
}
}
plrctrls_after_game_logic();
}
@ -1802,6 +1901,127 @@ const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueCha
} // namespace
bool HasActiveMonstersForAutoSave();
bool IsAutoSaveSafe()
{
if (gbIsMultiplayer || !gbRunGame)
return false;
if (!hasEnteredActiveGameplay)
return false;
if (!HaveTicksPassed(autoSaveCooldownUntil))
return false;
if (!HaveTicksPassed(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 && HasActiveMonstersForAutoSave())
return false;
return true;
}
void MarkCombatActivity()
{
autoSaveCombatCooldownUntil = SDL_GetTicks() + AutoSaveCombatCooldownMilliseconds;
}
bool HasActiveMonstersForAutoSave()
{
for (const Monster &monster : Monsters) {
if (monster.hitPoints <= 0 || monster.mode == MonsterMode::Death || monster.mode == MonsterMode::Petrified)
continue;
if (monster.activeForTicks == 0)
continue;
return true;
}
return false;
}
int GetSecondsUntilNextAutoSave()
{
if (!*GetOptions().Gameplay.autoSaveEnabled)
return -1;
if (HasPendingAutoSave())
return 0;
const TickCount now = SDL_GetTicks();
if (HaveTicksPassed(now, autoSaveNextTimerDueAt))
return 0;
const TickCount 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);
}
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 TickCount currentTime = SDL_GetTicks();
const SaveResult saveResult = SaveGame(SaveKind::Auto);
const TickCount afterSaveTime = SDL_GetTicks();
const bool saveSucceeded = saveResult == SaveResult::Success;
autoSaveCooldownUntil = afterSaveTime + AutoSaveCooldownMilliseconds;
if (saveSucceeded) {
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 saveSucceeded;
}
void InitKeymapActions()
{
Options &options = GetOptions();
@ -3434,6 +3654,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 {};
}

15
Source/diablo.h

@ -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;
@ -101,6 +109,13 @@ 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();
void RequestAutoSave(AutoSaveReason reason);
void QueueAutoSave(AutoSaveReason reason);
bool AttemptAutoSave(AutoSaveReason reason);
void PrintScreen(SDL_Keycode vkey);
/**

58
Source/gamemenu.cpp

@ -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"
@ -36,6 +40,9 @@ bool isGameMenuOpen = false;
namespace {
constexpr const char *SaveFailedPreservedMessage = N_("Save failed. The previous save is still available.");
constexpr const char *SaveFailedNoValidMessage = N_("Save failed. No valid save is available.");
// Forward-declare menu handlers, used by the global menu structs below.
void GamemenuPrevious(bool bActivate);
void GamemenuNewGame(bool bActivate);
@ -89,6 +96,8 @@ const char *const SoundToggleNames[] = {
N_("Sound Disabled"),
};
std::string saveGameMenuLabel;
void GamemenuUpdateSingle()
{
sgSingleMenu[2].setEnabled(gbValidSaveFile);
@ -98,6 +107,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();
@ -350,12 +380,26 @@ void gamemenu_save_game(bool /*bActivate*/)
RedrawEverything();
DrawAndBlit();
const uint32_t currentTime = SDL_GetTicks();
SaveGame();
const SaveResult saveResult = SaveGame(SaveKind::Manual);
ClrDiabloMsg();
InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks());
switch (saveResult) {
case SaveResult::Success: {
const uint32_t afterSaveTime = SDL_GetTicks();
const int timeElapsed = static_cast<int>(afterSaveTime - currentTime);
const int displayTime = std::max(500, 1000 - timeElapsed);
InitDiabloMsg(EMSG_GAME_SAVED, displayTime);
break;
}
case SaveResult::FailedButPreviousSavePreserved:
InitDiabloMsg(_(SaveFailedPreservedMessage));
break;
case SaveResult::FailedNoValidSave:
InitDiabloMsg(_(SaveFailedNoValidMessage));
break;
}
RedrawEverything();
NewCursor(CURSOR_HAND);
if (CornerStone.activated) {
if (saveResult == SaveResult::Success && CornerStone.activated) {
CornerstoneSave();
if (!demo::IsRunning()) SaveOptions();
}
@ -363,6 +407,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;

5
Source/gamemenu.h

@ -5,8 +5,12 @@
*/
#pragma once
#include <string_view>
namespace devilution {
struct TMenuItem;
void gamemenu_on();
void gamemenu_off();
void gamemenu_handle_previous();
@ -14,6 +18,7 @@ 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;

6
Source/gmenu.cpp

@ -29,6 +29,7 @@
#include "engine/render/clx_render.hpp"
#include "engine/render/primitive_render.hpp"
#include "engine/render/text_render.hpp"
#include "gamemenu.h"
#include "headless_mode.hpp"
#include "options.h"
#include "stores.h"
@ -123,11 +124,12 @@ int GmenuGetLineWidth(TMenuItem *pItem)
if (pItem->isSlider())
return SliderItemWidth;
return GetLineWidth(_(pItem->pszStr), GameFont46, 2);
return GetLineWidth(GetGamemenuText(*pItem), GameFont46, 2);
}
void GmenuDrawMenuItem(const Surface &out, TMenuItem *pItem, int y)
{
const std::string_view menuText = GetGamemenuText(*pItem);
const int w = GmenuGetLineWidth(pItem);
if (pItem->isSlider()) {
const int uiPositionX = GetUIRectangle().position.x;
@ -142,7 +144,7 @@ void GmenuDrawMenuItem(const Surface &out, TMenuItem *pItem, int y)
const int x = (gnScreenWidth - w) / 2;
const UiFlags style = pItem->enabled() ? UiFlags::ColorGold : UiFlags::ColorBlack;
DrawString(out, _(pItem->pszStr), Point { x, y },
DrawString(out, menuText, Point { x, y },
{ .flags = style | UiFlags::FontSize46, .spacing = 2 });
if (pItem == sgpCurrItem) {
const ClxSprite sprite = (*PentSpin_cel)[PentSpn2Spin()];

8
Source/inv.cpp

@ -22,6 +22,7 @@
#include "controls/control_mode.hpp"
#include "controls/plrctrls.h"
#include "cursor.h"
#include "diablo.h"
#include "engine/backbuffer_state.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/load_cel.hpp"
@ -1691,8 +1692,12 @@ void InvGetItem(Player &player, int ii)
NewCursor(player.HoldItem);
}
const bool pickedUniqueItem = &player == MyPlayer && item._iMagical == ITEM_QUALITY_UNIQUE;
// This potentially moves items in memory so must be done after we've made a copy
CleanupItems(ii);
if (pickedUniqueItem)
RequestAutoSave(AutoSaveReason::UniquePickup);
pcursitem = -1;
}
@ -1771,7 +1776,10 @@ void AutoGetItem(Player &player, Item *itemPointer, int ii)
PlaySFX(SfxID::GrabItem);
}
const bool pickedUniqueItem = &player == MyPlayer && item._iMagical == ITEM_QUALITY_UNIQUE;
CleanupItems(ii);
if (pickedUniqueItem)
RequestAutoSave(AutoSaveReason::UniquePickup);
return;
}

71
Source/loadsave.cpp

@ -57,6 +57,41 @@ constexpr size_t PlayerWalkPathSizeForSaveGame = 25;
uint8_t giNumberQuests;
uint8_t giNumberOfSmithPremiumItems;
bool ActiveSaveContainsGame()
{
if (gbIsMultiplayer)
return false;
auto archive = OpenSaveArchive(gSaveNumber);
if (!archive)
return false;
auto gameData = ReadArchive(*archive, "game");
if (gameData == nullptr)
return false;
return IsHeaderValid(LoadLE32(gameData.get()));
}
bool ActiveSaveContainsStash()
{
auto archive = OpenStashArchive();
if (!archive)
return true;
const char *stashFileName = gbIsMultiplayer ? "mpstashitems" : "spstashitems";
return ReadArchive(*archive, stashFileName) != nullptr;
}
SaveResult GetSaveFailureResult()
{
const bool hasValidGame = ActiveSaveContainsGame();
const bool hasValidStash = ActiveSaveContainsStash();
gbValidSaveFile = hasValidGame;
return (hasValidGame && hasValidStash) ? SaveResult::FailedButPreviousSavePreserved : SaveResult::FailedNoValidSave;
}
template <class T>
T SwapLE(T in)
{
@ -2680,7 +2715,7 @@ tl::expected<void, std::string> LoadGame(bool firstflag)
gbProcessPlayers = IsDiabloAlive(!firstflag);
if (gbIsHellfireSaveGame != gbIsHellfire) {
SaveGame();
SaveGame(SaveKind::System);
}
gbIsHellfireSaveGame = gbIsHellfire;
@ -2926,11 +2961,41 @@ void SaveGameData(SaveWriter &saveWriter)
SaveLevelSeeds(saveWriter);
}
void SaveGame()
SaveResult SaveGame(SaveKind kind)
{
gbValidSaveFile = true;
#if defined(UNPACKED_SAVES) && defined(DVL_NO_FILESYSTEM)
pfile_write_hero(/*writeGameData=*/true);
sfile_write_stash();
gbValidSaveFile = true;
return SaveResult::Success;
#else
switch (kind) {
case SaveKind::Manual:
if (!pfile_write_manual_game_with_backup())
return GetSaveFailureResult();
break;
case SaveKind::Auto:
case SaveKind::System:
if (!pfile_write_auto_game())
return GetSaveFailureResult();
break;
}
gbValidSaveFile = true;
switch (kind) {
case SaveKind::Manual:
if (!pfile_write_manual_stash_with_backup())
return GetSaveFailureResult();
break;
case SaveKind::Auto:
case SaveKind::System:
if (!pfile_write_auto_stash())
return GetSaveFailureResult();
break;
}
return SaveResult::Success;
#endif
}
void SaveLevel(SaveWriter &saveWriter)

14
Source/loadsave.h

@ -18,6 +18,18 @@ namespace devilution {
extern DVL_API_FOR_TEST bool gbIsHellfireSaveGame;
extern DVL_API_FOR_TEST uint8_t giNumberOfLevels;
enum class SaveKind : uint8_t {
Manual,
Auto,
System,
};
enum class SaveResult : uint8_t {
Success,
FailedButPreviousSavePreserved,
FailedNoValidSave,
};
void RemoveInvalidItem(Item &pItem);
_item_indexes RemapItemIdxFromDiablo(_item_indexes i);
_item_indexes RemapItemIdxToDiablo(_item_indexes i);
@ -40,7 +52,7 @@ tl::expected<void, std::string> LoadGame(bool firstflag);
void SaveHotkeys(SaveWriter &saveWriter, const Player &player);
void SaveHeroItems(SaveWriter &saveWriter, Player &player);
void SaveGameData(SaveWriter &saveWriter);
void SaveGame();
SaveResult SaveGame(SaveKind kind);
void SaveLevel(SaveWriter &saveWriter);
tl::expected<void, std::string> LoadLevel();
tl::expected<void, std::string> ConvertLevels(SaveWriter &saveWriter);

2
Source/monster.cpp

@ -4025,6 +4025,8 @@ void MonsterDeath(Monster &monster, Direction md, bool sendmsg)
M_ClearSquares(monster);
monster.occupyTile(monster.position.tile, false);
CheckQuestKill(monster, sendmsg);
if (!gbIsMultiplayer && IsAnyOf(monster.type().type, MT_CLEAVER, MT_SKING, MT_DIABLO, MT_DEFILER, MT_NAKRUL))
RequestAutoSave(AutoSaveReason::BossKill);
M_FallenFear(monster.position.tile);
if (IsAnyOf(monster.type().type, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD))
AddMissile(monster.position.tile, { 0, 0 }, Direction::South, MissileID::AcidPuddle, TARGET_PLAYERS, monster, monster.intelligence + 1, 0);

10
Source/options.cpp

@ -865,6 +865,12 @@ GameplayOptions::GameplayOptions()
, autoRefillBelt("Auto Refill Belt", OptionEntryFlags::None, N_("Auto Refill Belt"), N_("Refill belt from inventory when belt item is consumed."), false)
, disableCripplingShrines("Disable Crippling Shrines", OptionEntryFlags::None, N_("Disable Crippling Shrines"), N_("When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, Sacred Shrines and Murphy's Shrines are not able to be clicked on and labeled as disabled."), false)
, quickCast("Quick Cast", OptionEntryFlags::None, N_("Quick Cast"), N_("Spell hotkeys instantly cast the spell, rather than switching the readied spell."), false)
, autoSaveEnabled("Auto Save", OptionEntryFlags::CantChangeInMultiPlayer, N_("Auto Save"), N_("Autosave works only in single player and only at safe moments."), false)
#ifdef _DEBUG
, autoSaveIntervalSeconds("Auto Save Interval", OptionEntryFlags::CantChangeInMultiPlayer, N_("Autosave interval (seconds)"), N_("Time between periodic autosave attempts."), 120, { 30, 60, 90, 120, 180, 300, 600 })
#else
, autoSaveIntervalSeconds("Auto Save Interval", OptionEntryFlags::CantChangeInMultiPlayer | OptionEntryFlags::Invisible, "", "", 120, { 30, 60, 90, 120, 180, 300, 600 })
#endif
, numHealPotionPickup("Heal Potion Pickup", OptionEntryFlags::None, N_("Heal Potion Pickup"), N_("Number of Healing potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 })
, numFullHealPotionPickup("Full Heal Potion Pickup", OptionEntryFlags::None, N_("Full Heal Potion Pickup"), N_("Number of Full Healing potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 })
, numManaPotionPickup("Mana Potion Pickup", OptionEntryFlags::None, N_("Mana Potion Pickup"), N_("Number of Mana potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 })
@ -886,6 +892,10 @@ std::vector<OptionEntryBase *> GameplayOptions::GetEntries()
&cowQuest,
&runInTown,
&quickCast,
&autoSaveEnabled,
#ifdef _DEBUG
&autoSaveIntervalSeconds,
#endif
&testBard,
&testBarbarian,
&experienceBar,

4
Source/options.h

@ -623,6 +623,10 @@ struct GameplayOptions : OptionCategoryBase {
OptionEntryBoolean disableCripplingShrines;
/** @brief Spell hotkeys instantly cast the spell. */
OptionEntryBoolean quickCast;
/** @brief Enable periodic and event-driven automatic save in single player. */
OptionEntryBoolean autoSaveEnabled;
/** @brief Time in seconds between automatic save attempts. */
OptionEntryInt<int> autoSaveIntervalSeconds;
/** @brief Number of Healing potions to pick up automatically */
OptionEntryInt<int> numHealPotionPickup;
/** @brief Number of Full Healing potions to pick up automatically */

187
Source/pfile.cpp

@ -76,9 +76,9 @@ std::string GetSavePath(uint32_t saveNum, std::string_view savePrefix = {})
);
}
std::string GetStashSavePath()
std::string GetStashSavePath(std::string_view savePrefix = {})
{
return StrCat(paths::PrefPath(),
return StrCat(paths::PrefPath(), savePrefix,
gbIsSpawn ? "stash_spawn" : "stash",
#ifdef UNPACKED_SAVES
gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR
@ -170,22 +170,57 @@ SaveWriter GetStashWriter()
return SaveWriter(GetStashSavePath());
}
#ifndef DISABLE_DEMOMODE
void CopySaveFile(uint32_t saveNum, std::string targetPath)
#if !(defined(UNPACKED_SAVES) && defined(DVL_NO_FILESYSTEM))
bool SaveLocationExists(const std::string &location)
{
return FileExists(location.c_str()) || DirectoryExists(location.c_str());
}
void CopySaveLocation(const std::string &sourceLocation, const std::string &targetLocation)
{
const std::string savePath = GetSavePath(saveNum);
#if defined(UNPACKED_SAVES)
#ifdef DVL_NO_FILESYSTEM
#error "UNPACKED_SAVES requires either DISABLE_DEMOMODE or C++17 <filesystem>"
if (!targetLocation.empty()) {
CreateDir(targetLocation.c_str());
}
for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(sourceLocation)) {
const std::filesystem::path targetFilePath = std::filesystem::path(targetLocation) / entry.path().filename();
CopyFileOverwrite(entry.path().string().c_str(), targetFilePath.string().c_str());
}
#else
CopyFileOverwrite(sourceLocation.c_str(), targetLocation.c_str());
#endif
if (!targetPath.empty()) {
CreateDir(targetPath.c_str());
}
void RestoreSaveLocation(const std::string &targetLocation, const std::string &backupLocation)
{
#if defined(UNPACKED_SAVES)
if (DirectoryExists(targetLocation.c_str())) {
for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(targetLocation))
RemoveFile(entry.path().string().c_str());
}
for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(savePath)) {
CopyFileOverwrite(entry.path().string().c_str(), (targetPath + entry.path().filename().string()).c_str());
CreateDir(targetLocation.c_str());
for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(backupLocation)) {
const std::filesystem::path restoredFilePath = std::filesystem::path(targetLocation) / entry.path().filename();
CopyFileOverwrite(entry.path().string().c_str(), restoredFilePath.string().c_str());
}
#else
CopyFileOverwrite(savePath.c_str(), targetPath.c_str());
CopyFileOverwrite(backupLocation.c_str(), targetLocation.c_str());
#endif
}
void DeleteSaveLocation(const std::string &location)
{
#if defined(UNPACKED_SAVES)
if (!DirectoryExists(location.c_str()))
return;
for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(location))
RemoveFile(entry.path().string().c_str());
std::filesystem::remove(location);
#else
if (FileExists(location.c_str()))
RemoveFile(location.c_str());
#endif
}
#endif
@ -629,12 +664,118 @@ void pfile_write_hero(bool writeGameData)
pfile_write_hero(saveWriter, writeGameData);
}
#if !(defined(UNPACKED_SAVES) && defined(DVL_NO_FILESYSTEM))
bool SaveWrittenGameIsValid()
{
auto archive = OpenSaveArchive(gSaveNumber);
return archive && ArchiveContainsGame(*archive);
}
bool WriteGameAndRestoreOnFailure(const std::string &restoreLocation)
{
const bool hasRestoreLocation = SaveLocationExists(restoreLocation);
pfile_write_hero(/*writeGameData=*/true);
if (SaveWrittenGameIsValid())
return true;
if (!hasRestoreLocation)
return false;
RestoreSaveLocation(GetSavePath(gSaveNumber), restoreLocation);
return false;
}
bool pfile_write_manual_game_with_backup()
{
const std::string backupPrefix = "backup_";
const std::string backupLocation = GetSavePath(gSaveNumber, backupPrefix);
const std::string saveLocation = GetSavePath(gSaveNumber);
if (SaveLocationExists(saveLocation))
CopySaveLocation(saveLocation, backupLocation);
return WriteGameAndRestoreOnFailure(backupLocation);
}
bool pfile_write_auto_game()
{
const std::string restorePrefix = "autosave_restore_";
const std::string restoreLocation = GetSavePath(gSaveNumber, restorePrefix);
const std::string saveLocation = GetSavePath(gSaveNumber);
if (SaveLocationExists(saveLocation))
CopySaveLocation(saveLocation, restoreLocation);
const bool saveIsValid = WriteGameAndRestoreOnFailure(restoreLocation);
DeleteSaveLocation(restoreLocation);
return saveIsValid;
}
bool WriteStashAndRestoreOnFailure(std::string_view restorePrefix, bool deleteRestoreLocationOnSuccess)
{
if (!Stash.dirty)
return true;
const std::string restoreLocation = GetStashSavePath(restorePrefix);
const std::string stashLocation = GetStashSavePath();
SaveWriter stashWriter = GetStashWriter();
SaveStash(stashWriter);
auto archive = OpenStashArchive();
const char *stashFileName = gbIsMultiplayer ? "mpstashitems" : "spstashitems";
const bool stashIsValid = archive && ReadArchive(*archive, stashFileName) != nullptr;
if (stashIsValid) {
Stash.dirty = false;
if (deleteRestoreLocationOnSuccess)
DeleteSaveLocation(restoreLocation);
return true;
}
if (!SaveLocationExists(restoreLocation))
return false;
RestoreSaveLocation(stashLocation, restoreLocation);
return false;
}
bool pfile_write_manual_stash_with_backup()
{
const std::string backupPrefix = "backup_";
const std::string backupLocation = GetStashSavePath(backupPrefix);
const std::string stashLocation = GetStashSavePath();
if (SaveLocationExists(stashLocation))
CopySaveLocation(stashLocation, backupLocation);
return WriteStashAndRestoreOnFailure(backupPrefix, false);
}
bool pfile_write_auto_stash()
{
const std::string restorePrefix = "autosave_restore_";
const std::string restoreLocation = GetStashSavePath(restorePrefix);
const std::string stashLocation = GetStashSavePath();
if (SaveLocationExists(stashLocation))
CopySaveLocation(stashLocation, restoreLocation);
const bool stashIsValid = WriteStashAndRestoreOnFailure(restorePrefix, true);
if (!stashIsValid && SaveLocationExists(restoreLocation))
DeleteSaveLocation(restoreLocation);
return stashIsValid;
}
#endif
#ifndef DISABLE_DEMOMODE
#if !(defined(UNPACKED_SAVES) && defined(DVL_NO_FILESYSTEM))
void pfile_write_hero_demo(int demo)
{
const std::string savePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_"));
CopySaveFile(gSaveNumber, savePath);
auto saveWriter = SaveWriter(savePath.c_str());
const std::string saveLocation = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_"));
CopySaveLocation(GetSavePath(gSaveNumber), saveLocation);
auto saveWriter = SaveWriter(saveLocation.c_str());
pfile_write_hero(saveWriter, true);
}
@ -647,13 +788,27 @@ HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails)
const std::string actualSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_actual_"));
{
CopySaveFile(gSaveNumber, actualSavePath);
CopySaveLocation(GetSavePath(gSaveNumber), actualSavePath);
SaveWriter saveWriter(actualSavePath.c_str());
pfile_write_hero(saveWriter, true);
}
return CompareSaves(actualSavePath, referenceSavePath, logDetails);
}
#else
// Demo save comparison is unavailable on UNPACKED_SAVES targets without filesystem support.
void pfile_write_hero_demo(int demo)
{
(void)demo;
}
HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails)
{
(void)demo;
(void)logDetails;
return { HeroCompareResult::ReferenceNotFound, {} };
}
#endif
#endif
void sfile_write_stash()

4
Source/pfile.h

@ -101,6 +101,10 @@ std::optional<SaveReader> OpenStashArchive();
const char *pfile_get_password();
std::unique_ptr<std::byte[]> ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen = nullptr);
void pfile_write_hero(bool writeGameData = false);
bool pfile_write_manual_game_with_backup();
bool pfile_write_auto_game();
bool pfile_write_manual_stash_with_backup();
bool pfile_write_auto_stash();
#ifndef DISABLE_DEMOMODE
/**

25
Source/player.cpp

@ -24,6 +24,7 @@
#ifdef _DEBUG
#include "debug.h"
#endif
#include "diablo.h"
#include "engine/backbuffer_state.hpp"
#include "engine/load_cl2.hpp"
#include "engine/load_file.hpp"
@ -179,6 +180,9 @@ void StartAttack(Player &player, Direction d, bool includesFirstFrame)
return;
}
if (&player == MyPlayer && IsAnyOf(LastPlayerAction, PlayerActionType::AttackMonsterTarget, PlayerActionType::AttackPlayerTarget))
MarkCombatActivity();
int8_t skippedAnimationFrames = 0;
const auto flags = player._pIFlags;
@ -211,6 +215,9 @@ void StartRangeAttack(Player &player, Direction d, WorldTileCoord cx, WorldTileC
return;
}
if (&player == MyPlayer)
MarkCombatActivity();
int8_t skippedAnimationFrames = 0;
const auto flags = player._pIFlags;
@ -246,6 +253,17 @@ player_graphic GetPlayerGraphicForSpell(SpellID spellId)
}
}
bool IsSpellOffensiveForAutoSave(SpellID spellId)
{
const SpellData &spellData = GetSpellData(spellId);
for (MissileID missileId : spellData.sMissiles) {
if (missileId != MissileID::Null)
return true;
}
return false;
}
void StartSpell(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord cy)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
@ -272,6 +290,9 @@ void StartSpell(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord c
if (!isValid)
return;
if (&player == MyPlayer && IsSpellOffensiveForAutoSave(player.queuedSpell.spellId))
MarkCombatActivity();
auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending;
if (player._pmode == PM_SPELL)
animationFlags = static_cast<AnimationDistributionFlags>(animationFlags | AnimationDistributionFlags::RepeatedAction);
@ -761,6 +782,8 @@ bool PlrHitPlr(Player &attacker, Player &target)
if (&attacker == MyPlayer) {
NetSendCmdDamage(true, target, skdam, DamageType::Physical);
}
if (&attacker == MyPlayer && skdam > 0)
MarkCombatActivity();
StartPlrHit(target, skdam, false);
return true;
@ -2823,6 +2846,8 @@ void StripTopGold(Player &player)
void ApplyPlrDamage(DamageType damageType, Player &player, int dam, int minHP /*= 0*/, int frac /*= 0*/, DeathReason deathReason /*= DeathReason::MonsterOrTrap*/)
{
int totalDamage = (dam << 6) + frac;
if (&player == MyPlayer && totalDamage > 0)
MarkCombatActivity();
if (&player == MyPlayer && !player.hasNoLife()) {
lua::OnPlayerTakeDamage(&player, totalDamage, static_cast<int>(damageType));
}

4
Source/utils/sdl2_backports.h

@ -7,6 +7,10 @@
#define SDL_MAX_UINT32 ((Uint32)0xFFFFFFFFu)
#endif
#ifndef SDL_TICKS_PASSED
#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0)
#endif
#if !SDL_VERSION_ATLEAST(2, 0, 4)
inline SDL_bool SDL_PointInRect(const SDL_Point *p, const SDL_Rect *r)
{

1
Source/utils/sdl2_to_1_2_backports.h

@ -26,6 +26,7 @@
#define SDL_floor floor
#define SDL_MAX_UINT32 ((Uint32)0xFFFFFFFFu)
#define SDL_TICKS_PASSED(A, B) ((Sint32)((B) - (A)) <= 0)
//== Events handling

Loading…
Cancel
Save