Open Track integrity of assets that modify game engine logic

This commit is contained in:
Stephen C. Wills
2026-06-22 00:02:05 +02:00
committed by GitHub
parent 15d0e67e26
commit e2a9dbd6ee
7 changed files with 92 additions and 11 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ tl::expected<DataFile, DataFile::Error> DataFile::load(std::string_view path)
// TODO: It should be possible to stream the data file contents instead of copying the whole thing into memory
std::unique_ptr<char[]> data { new char[size] };
{
AssetHandle handle = OpenAsset(std::move(ref));
AssetHandle handle = OpenIntegralAsset(std::move(ref));
if (!handle.ok())
return tl::unexpected { Error::OpenFailed };
if (size > 0 && !handle.read(data.get(), size))
+51 -1
View File
@@ -35,6 +35,7 @@ namespace devilution {
std::vector<std::string> OverridePaths;
std::map<int, MpqArchiveT, std::greater<>> MpqArchives;
bool HasHellfireMpq;
bool IsAssetIntegrityViolated = false;
namespace {
@@ -149,6 +150,7 @@ AssetRef FindAsset(std::string_view filename)
result.directHandle = OpenOptionalRWops(path);
if (result.directHandle != nullptr) {
LogVerbose("Loaded MPQ file override: {}", path);
result.isOverridden = true;
return result;
}
}
@@ -181,7 +183,7 @@ AssetRef FindAsset(std::string_view filename)
AssetHandle OpenAsset(AssetRef &&ref, bool threadsafe)
{
#if UNPACKED_MPQS
#ifdef UNPACKED_MPQS
return AssetHandle { OpenFile(ref.path, "rb") };
#else
if (ref.archive != nullptr)
@@ -213,6 +215,32 @@ AssetHandle OpenAsset(std::string_view filename, size_t &fileSize, bool threadsa
return OpenAsset(std::move(ref), threadsafe);
}
AssetHandle OpenIntegralAsset(AssetRef &&ref, bool threadsafe)
{
#ifndef UNPACKED_MPQS
if (ref.isOverridden)
IsAssetIntegrityViolated = true;
#endif
return OpenAsset(std::move(ref), threadsafe);
}
AssetHandle OpenIntegralAsset(std::string_view filename, bool threadsafe)
{
AssetRef ref = FindAsset(filename);
if (!ref.ok())
return AssetHandle {};
return OpenIntegralAsset(std::move(ref), threadsafe);
}
AssetHandle OpenIntegralAsset(std::string_view filename, size_t &fileSize, bool threadsafe)
{
AssetRef ref = FindAsset(filename);
if (!ref.ok())
return AssetHandle {};
fileSize = ref.size();
return OpenIntegralAsset(std::move(ref), threadsafe);
}
SDL_IOStream *OpenAssetAsSdlRwOps(std::string_view filename, bool threadsafe)
{
#ifdef UNPACKED_MPQS
@@ -247,6 +275,28 @@ tl::expected<AssetData, std::string> LoadAsset(std::string_view path)
return AssetData { std::move(data), size };
}
tl::expected<AssetData, std::string> LoadIntegralAsset(std::string_view path)
{
AssetRef ref = FindAsset(path);
if (!ref.ok()) {
return tl::make_unexpected(StrCat("Asset not found: ", path));
}
const size_t size = ref.size();
std::unique_ptr<char[]> data { new char[size] };
AssetHandle handle = OpenIntegralAsset(std::move(ref));
if (!handle.ok()) {
return tl::make_unexpected(StrCat("Failed to open asset: ", path, "\n", handle.error()));
}
if (size > 0 && !handle.read(data.get(), size)) {
return tl::make_unexpected(StrCat("Read failed: ", path, "\n", handle.error()));
}
return AssetData { std::move(data), size };
}
std::string FailedToOpenFileErrorMessage(std::string_view path, std::string_view error)
{
return fmt::format(fmt::runtime(_("Failed to open file:\n{:s}\n\n{:s}\n\nThe MPQ file(s) might be damaged. Please check the file integrity.")), path, error);
+6
View File
@@ -120,6 +120,7 @@ struct AssetRef {
MpqArchive *archive = nullptr;
uint32_t hashIndex = UINT32_MAX;
std::string_view filename;
bool isOverridden = false;
// Alternatively, a direct SDL_IOStream handle:
SDL_IOStream *directHandle = nullptr;
@@ -279,6 +280,9 @@ AssetRef FindAsset(std::string_view filename);
AssetHandle OpenAsset(AssetRef &&ref, bool threadsafe = false);
AssetHandle OpenAsset(std::string_view filename, bool threadsafe = false);
AssetHandle OpenAsset(std::string_view filename, size_t &fileSize, bool threadsafe = false);
AssetHandle OpenIntegralAsset(AssetRef &&ref, bool threadsafe = false);
AssetHandle OpenIntegralAsset(std::string_view filename, bool threadsafe = false);
AssetHandle OpenIntegralAsset(std::string_view filename, size_t &fileSize, bool threadsafe = false);
SDL_IOStream *OpenAssetAsSdlRwOps(std::string_view filename, bool threadsafe = false);
@@ -293,6 +297,7 @@ struct AssetData {
};
tl::expected<AssetData, std::string> LoadAsset(std::string_view path);
tl::expected<AssetData, std::string> LoadIntegralAsset(std::string_view path);
#ifdef UNPACKED_MPQS
using MpqArchiveT = std::string;
@@ -306,6 +311,7 @@ constexpr int DevilutionXMpqPriority = 9000;
constexpr int LangMpqPriority = 9100;
constexpr int FontMpqPriority = 9200;
extern bool HasHellfireMpq;
extern bool IsAssetIntegrityViolated;
void LoadCoreArchives();
void LoadLanguageArchive();
+18
View File
@@ -35,6 +35,24 @@ tl::expected<void, std::string> LoadFileInMemWithStatus(const char *path, T *dat
return {};
}
template <typename T>
tl::expected<void, std::string> LoadIntegralFileInMemWithStatus(const char *path, T *data)
{
size_t size;
AssetHandle handle = OpenIntegralAsset(path, size);
if (!handle.ok()) {
if (HeadlessMode) return {};
return tl::make_unexpected(FailedToOpenFileErrorMessage(path, handle.error()));
}
if ((size % sizeof(T)) != 0) {
return tl::make_unexpected(StrCat("File size does not align with type\n", path));
}
if (!handle.read(data, size)) {
return tl::make_unexpected("handle.read failed");
}
return {};
}
template <typename T>
void LoadFileInMem(const char *path, T *data)
{
+8 -8
View File
@@ -443,12 +443,12 @@ tl::expected<void, std::string> LoadLevelSOLData()
{
switch (leveltype) {
case DTYPE_TOWN:
if (!LoadFileInMemWithStatus("nlevels\\towndata\\town.sol", SOLData).has_value()) {
RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\towndata\\town.sol", SOLData));
if (!LoadIntegralFileInMemWithStatus("nlevels\\towndata\\town.sol", SOLData).has_value()) {
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("levels\\towndata\\town.sol", SOLData));
}
break;
case DTYPE_CATHEDRAL:
RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l1data\\l1.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("levels\\l1data\\l1.sol", SOLData));
// Fix incorrectly marked arched tiles
SOLData[9] |= TileProperties::BlockLight | TileProperties::BlockMissile;
SOLData[15] |= TileProperties::BlockLight | TileProperties::BlockMissile;
@@ -476,10 +476,10 @@ tl::expected<void, std::string> LoadLevelSOLData()
SOLData[450] |= TileProperties::BlockLight | TileProperties::BlockMissile;
break;
case DTYPE_CATACOMBS:
RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l2data\\l2.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("levels\\l2data\\l2.sol", SOLData));
break;
case DTYPE_CAVES:
RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l3data\\l3.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("levels\\l3data\\l3.sol", SOLData));
// The graphics for tile 48 sub-tile 171 frame 461 are partly incorrect, as they
// have a few pixels that should belong to the solid tile 49 instead.
// Marks the sub-tile as "BlockMissile" to avoid treating it as a floor during rendering.
@@ -490,14 +490,14 @@ tl::expected<void, std::string> LoadLevelSOLData()
SOLData[487] |= TileProperties::Solid;
break;
case DTYPE_HELL:
RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l4data\\l4.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("levels\\l4data\\l4.sol", SOLData));
SOLData[210] = TileProperties::None; // Tile is incorrectly marked as being solid
break;
case DTYPE_NEST:
RETURN_IF_ERROR(LoadFileInMemWithStatus("nlevels\\l6data\\l6.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("nlevels\\l6data\\l6.sol", SOLData));
break;
case DTYPE_CRYPT:
RETURN_IF_ERROR(LoadFileInMemWithStatus("nlevels\\l5data\\l5.sol", SOLData));
RETURN_IF_ERROR(LoadIntegralFileInMemWithStatus("nlevels\\l5data\\l5.sol", SOLData));
SOLData[142] = TileProperties::None; // Tile is incorrectly marked as being solid
break;
default:
+1 -1
View File
@@ -113,7 +113,7 @@ sol::object LuaLoadScriptFromAssets(std::string_view packageName)
return luaState.sol.load(iter->second.as_string_view(), path, sol::load_mode::binary);
}
tl::expected<AssetData, std::string> assetData = LoadAsset(path);
tl::expected<AssetData, std::string> assetData = LoadIntegralAsset(path);
if (!assetData.has_value()) {
sol::stack::push(luaState.sol.lua_state(), assetData.error());
return sol::stack_object(luaState.sol.lua_state(), -1);
+7
View File
@@ -14,6 +14,7 @@
#endif
#include "DiabloUI/diabloui.h"
#include "DiabloUI/selok.h"
#include "DiabloUI/settingsmenu.h"
#include "engine/assets.hpp"
#include "engine/demomode.h"
@@ -80,6 +81,12 @@ bool InitSinglePlayerMenu()
bool InitMultiPlayerMenu()
{
#ifndef _DEBUG
if (IsAssetIntegrityViolated) {
UiSelOkDialog(_("Multi Player Game").data(), _("Cannot play Multiplayer with overridden *.lua, *.tsv, or *.sol assets.").data(), false);
return true;
}
#endif
gbIsMultiplayer = true;
return InitMenu(SELHERO_CONNECT);
}