diff --git a/CMakeLists.txt b/CMakeLists.txt
index 13e31277eb..ac8def072c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,6 +123,8 @@ add_library(${PROJECT_NAME} OBJECT
src/fileext_guesser.h
src/filesystem.cpp
src/filesystem.h
+ src/filesystem_hook.cpp
+ src/filesystem_hook.h
src/filesystem_lzh.cpp
src/filesystem_lzh.h
src/filesystem_native.cpp
diff --git a/Makefile.am b/Makefile.am
index 444e7c90f0..9426404206 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -103,6 +103,8 @@ libeasyrpg_player_a_SOURCES = \
src/fileext_guesser.h \
src/filesystem.cpp \
src/filesystem.h \
+ src/filesystem_hook.cpp \
+ src/filesystem_hook.h \
src/filesystem_lzh.cpp \
src/filesystem_lzh.h \
src/filesystem_native.cpp \
diff --git a/lib/liblcf-version b/lib/liblcf-version
new file mode 160000
index 0000000000..032ccdc3b7
--- /dev/null
+++ b/lib/liblcf-version
@@ -0,0 +1 @@
+Subproject commit 032ccdc3b793ecd28158c2128e0ee119ecc867dc
diff --git a/src/filesystem_hook.cpp b/src/filesystem_hook.cpp
new file mode 100644
index 0000000000..0d03c1b066
--- /dev/null
+++ b/src/filesystem_hook.cpp
@@ -0,0 +1,145 @@
+/*
+ * This file is part of EasyRPG Player.
+ *
+ * EasyRPG Player is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EasyRPG Player is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EasyRPG Player. If not, see .
+ */
+
+#include "filesystem_hook.h"
+#include "filesystem_stream.h"
+#include "options.h"
+
+#include
+#include
+#include
+
+class CaesarStreamBuf : public std::streambuf {
+public:
+ CaesarStreamBuf(std::streambuf* parent_sb, int n) : parent_sb(parent_sb), n(n) {
+ setg(&buf, &buf + 1, &buf + 1);
+ }
+ CaesarStreamBuf(CaesarStreamBuf const& other) = delete;
+ CaesarStreamBuf const& operator=(CaesarStreamBuf const& other) = delete;
+ ~CaesarStreamBuf() {
+ delete parent_sb;
+ }
+
+protected:
+ int_type underflow() override {
+ auto byte = parent_sb->sbumpc();
+
+ if (byte == traits_type::eof()) {
+ return byte;
+ }
+
+ buf = traits_type::to_char_type(byte);
+ buf -= n;
+
+ setg(&buf, &buf, &buf + 1);
+
+ return byte;
+ }
+
+ std::streambuf::pos_type seekoff(std::streambuf::off_type offset, std::ios_base::seekdir dir, std::ios_base::openmode) override {
+ return parent_sb->pubseekoff(offset, dir);
+ }
+
+ std::streambuf::pos_type seekpos(std::streambuf::pos_type pos, std::ios_base::openmode) override {
+ return parent_sb->pubseekpos(pos);
+ }
+
+private:
+ std::streambuf* parent_sb;
+
+ char buf;
+ int n;
+};
+
+
+HookFilesystem::HookFilesystem(FilesystemView parent_fs, Hook hook) : Filesystem("", parent_fs), active_hook(hook) {
+ // no-op
+}
+
+FilesystemView HookFilesystem::Detect(FilesystemView fs) {
+ auto lmt = fs.OpenInputStream(TREEMAP_NAME);
+ std::array buf;
+
+ lmt.ReadIntoObj(buf);
+
+ if (!memcmp(buf.data(), "\xbMdgNbqUsff", 11)) {
+ auto hook_fs = std::make_shared(fs, Hook::SacredTears);
+ return hook_fs->Subtree("");
+ }
+
+ return fs;
+}
+
+bool HookFilesystem::IsFile(StringView path) const {
+ return GetParent().IsFile(path);
+}
+
+bool HookFilesystem::IsDirectory(StringView path, bool follow_symlinks) const {
+ return GetParent().IsDirectory(path, follow_symlinks);
+}
+
+bool HookFilesystem::Exists(StringView path) const {
+ return GetParent().Exists(path);
+}
+
+int64_t HookFilesystem::GetFilesize(StringView path) const {
+ return GetParent().GetFilesize(path);
+}
+
+bool HookFilesystem::MakeDirectory(StringView dir, bool follow_symlinks) const {
+ return GetParent().MakeDirectory(dir, follow_symlinks);
+}
+
+bool HookFilesystem::IsFeatureSupported(Feature f) const {
+ return GetParent().IsFeatureSupported(f);
+}
+
+std::streambuf* HookFilesystem::CreateInputStreambuffer(StringView path, std::ios_base::openmode mode) const {
+ auto parent_sb = GetParent().CreateInputStreambuffer(path, mode);
+
+ if (active_hook == Hook::SacredTears) {
+ if (path == TREEMAP_NAME) {
+ return new CaesarStreamBuf(parent_sb, 1);
+ }
+ }
+
+ return parent_sb;
+}
+
+std::streambuf* HookFilesystem::CreateOutputStreambuffer(StringView path, std::ios_base::openmode mode) const {
+ return GetParent().CreateOutputStreambuffer(path, mode);
+}
+
+bool HookFilesystem::GetDirectoryContent(StringView path, std::vector& tree) const {
+ auto dir_tree = GetParent().ListDirectory(path);
+
+ if (!dir_tree) {
+ return false;
+ }
+
+ for (auto& item: *dir_tree) {
+ tree.push_back(item.second);
+ }
+
+ return true;
+}
+
+std::string HookFilesystem::Describe() const {
+ assert(active_hook == Hook::SacredTears);
+
+ return fmt::format("[Hook] ({})", "Sacred Tears");
+}
diff --git a/src/filesystem_hook.h b/src/filesystem_hook.h
new file mode 100644
index 0000000000..260116ae2f
--- /dev/null
+++ b/src/filesystem_hook.h
@@ -0,0 +1,60 @@
+/*
+ * This file is part of EasyRPG Player.
+ *
+ * EasyRPG Player is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EasyRPG Player is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EasyRPG Player. If not, see .
+ */
+
+#ifndef EP_FILESYSTEM_HOOK_H
+#define EP_FILESYSTEM_HOOK_H
+
+#include "filesystem.h"
+
+/**
+ * A virtual filesystem that applies modifications to game files
+ */
+class HookFilesystem : public Filesystem {
+public:
+ enum class Hook {
+ // The Sacred Tears: TRUE
+ // Shifts the offset of all bytes in the LMT back by one
+ SacredTears
+ };
+
+ explicit HookFilesystem(FilesystemView parent_fs, Hook hook);
+
+ static FilesystemView Detect(FilesystemView fs);
+
+ /**
+ * Implementation of abstract methods
+ */
+ /** @{ */
+ bool IsFile(StringView path) const override;
+ bool IsDirectory(StringView path, bool follow_symlinks) const override;
+ bool Exists(StringView path) const override;
+ int64_t GetFilesize(StringView path) const override;
+ bool MakeDirectory(StringView dir, bool follow_symlinks) const override;
+ bool IsFeatureSupported(Feature f) const override;
+ std::string Describe() const override;
+ /** @} */
+protected:
+ /** @{ */
+ bool GetDirectoryContent(StringView path, std::vector& entries) const override;
+ std::streambuf* CreateInputStreambuffer(StringView path, std::ios_base::openmode mode) const override;
+ std::streambuf* CreateOutputStreambuffer(StringView path, std::ios_base::openmode mode) const override;
+ /** @} */
+
+ Hook active_hook;
+};
+
+#endif
diff --git a/src/player.cpp b/src/player.cpp
index 7645c3f16b..95a5753ed5 100644
--- a/src/player.cpp
+++ b/src/player.cpp
@@ -24,7 +24,6 @@
#include
#include
#include
-#include
#ifdef _WIN32
# include "platform/windows/utils.h"
@@ -42,6 +41,7 @@
#include "filefinder.h"
#include "filefinder_rtp.h"
#include "fileext_guesser.h"
+#include "filesystem_hook.h"
#include "game_actors.h"
#include "game_battle.h"
#include "game_map.h"
@@ -692,6 +692,9 @@ void Player::CreateGameObjects() {
}
escape_char = Utils::DecodeUTF32(Player::escape_symbol).front();
+ // Special handling for games with altered files
+ FileFinder::SetGameFilesystem(HookFilesystem::Detect(FileFinder::Game()));
+
// Check for translation-related directories and load language names.
translation.InitTranslations();