diff --git a/CMakeLists.txt b/CMakeLists.txt index e91e8f92e4c..ac51d1788e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,8 @@ add_library(${PROJECT_NAME} OBJECT src/fileext_guesser.h src/filesystem.cpp src/filesystem.h + src/filesystem_lzh.cpp + src/filesystem_lzh.h src/filesystem_native.cpp src/filesystem_native.h src/filesystem_root.cpp @@ -819,7 +821,7 @@ endif() player_find_package(NAME PNG TARGET PNG::PNG REQUIRED) player_find_package(NAME fmt TARGET fmt::fmt REQUIRED) -# Do not use player_find_package. enable_language used by pixman on Android does work properly inside function calls +# Do not use player_find_package. enable_language used by pixman on Android does not work properly inside function calls find_package(Pixman REQUIRED) target_link_libraries(${PROJECT_NAME} PIXMAN::PIXMAN) @@ -849,6 +851,15 @@ if(TARGET freetype) CONFIG_BROKEN) endif() +# lzh archive support +option(PLAYER_WITH_LHASA "Support running games in lzh archives" ON) + +player_find_package(NAME lhasa + CONDITION PLAYER_WITH_LHASA + DEFINITION HAVE_LHASA + TARGET LHASA::liblhasa +) + # Sound system to use if(${PLAYER_TARGET_PLATFORM} STREQUAL "SDL2") set(PLAYER_AUDIO_BACKEND "SDL2" CACHE STRING "Audio system to use. Options: SDL2 OFF") @@ -1444,7 +1455,7 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii)$ if(WAV_LIBS) message(STATUS "WAV playback: ${WAV_LIBS}") else() - message(STATUS "WAV playback: None") + message(STATUS "WAV playback: No") endif() set(MIDI_LIBS) @@ -1467,13 +1478,13 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii)$ if(MIDI_LIBS) message(STATUS "MIDI playback: ${MIDI_LIBS}") else() - message(STATUS "MIDI playback: None") + message(STATUS "MIDI playback: No") endif() if(TARGET MPG123::libmpg123) message(STATUS "MP3 playback: mpg123") else() - message(STATUS "MP3 playback: None") + message(STATUS "MP3 playback: No") endif() if(TARGET Vorbis::vorbisfile) @@ -1481,19 +1492,19 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii)$ elseif(TARGET Tremor::Tremor) message(STATUS "Ogg Vorbis playback: tremor") else() - message(STATUS "Ogg Vorbis playback: None") + message(STATUS "Ogg Vorbis playback: No") endif() if(TARGET XMP::XMP) message(STATUS "MOD playback: libxmp") else() - message(STATUS "MOD playback: None") + message(STATUS "MOD playback: No") endif() if(TARGET OpusFile::opusfile) message(STATUS "Opus playback: opusfile") else() - message(STATUS "Opus playback: None") + message(STATUS "Opus playback: No") endif() if(TARGET speexdsp::speexdsp) @@ -1501,7 +1512,7 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii)$ elseif(TARGET Samplerate::Samplerate) message(STATUS "Resampler: libsamplerate") else() - message(STATUS "Resampler: None") + message(STATUS "Resampler: No") endif() endif() @@ -1515,6 +1526,12 @@ else() message(STATUS "Font rendering: built-in") endif() +if(TARGET LHASA::liblhasa) + message(STATUS "LZH archive support: lhasa") +else() + message(STATUS "LZH archive support: No") +endif() + message(STATUS "") message(STATUS "Manual page: ${MANUAL_STATUS}") diff --git a/Makefile.am b/Makefile.am index 4c4774892cc..c969c1c3c1f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -101,6 +101,8 @@ libeasyrpg_player_a_SOURCES = \ src/fileext_guesser.h \ src/filesystem.cpp \ src/filesystem.h \ + src/filesystem_lzh.cpp \ + src/filesystem_lzh.h \ src/filesystem_native.cpp \ src/filesystem_native.h \ src/filesystem_root.cpp \ @@ -571,6 +573,7 @@ libeasyrpg_player_a_CXXFLAGS = \ $(PIXMAN_CFLAGS) \ $(FREETYPE_CFLAGS) \ $(HARFBUZZ_CFLAGS) \ + $(LHASA_CFLAGS) \ $(SDL_CFLAGS) \ $(PNG_CFLAGS) \ $(ZLIB_CFLAGS) \ @@ -623,6 +626,7 @@ easyrpg_player_LDADD = libeasyrpg-player.a libplayer-version.a \ $(PIXMAN_LIBS) \ $(FREETYPE_LIBS) \ $(HARFBUZZ_LIBS) \ + $(LHASA_LIBS) \ $(SDL_LIBS) \ $(PNG_LIBS) \ $(ZLIB_LIBS) \ diff --git a/builds/cmake/Modules/Findlhasa.cmake b/builds/cmake/Modules/Findlhasa.cmake new file mode 100644 index 00000000000..57d3a83a279 --- /dev/null +++ b/builds/cmake/Modules/Findlhasa.cmake @@ -0,0 +1,64 @@ +#.rst: +# Findlhasa +# ----------- +# +# Find the lhasa Library +# +# Imported Targets +# ^^^^^^^^^^^^^^^^ +# +# This module defines the following :prop_tgt:`IMPORTED` targets: +# +# ``LHASA::liblhasa`` +# The ``lhasa`` library, if found. +# +# Result Variables +# ^^^^^^^^^^^^^^^^ +# +# This module will set the following variables in your project: +# +# ``LHASA_INCLUDE_DIRS`` +# where to find lhasa headers. +# ``LHASA_LIBRARIES`` +# the libraries to link against to use lhasa. +# ``LHASA_FOUND`` +# true if the lhasa headers and libraries were found. + +find_package(PkgConfig QUIET) + +pkg_check_modules(PC_LHASA QUIET liblhasa) + +# Look for the header file. +find_path(LHASA_INCLUDE_DIR + NAMES lhasa.h + PATH_SUFFIXES liblhasa-1.0 liblhasa + HINTS ${PC_LHASA_INCLUDE_DIRS}) + +# Look for the library. +# Allow LHASA_LIBRARY to be set manually, as the location of the lhasa library +if(NOT LHASA_LIBRARY) + find_library(LHASA_LIBRARY + NAMES liblhasa lhasa + HINTS ${PC_LHASA_LIBRARY_DIRS}) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(lhasa + REQUIRED_VARS LHASA_LIBRARY LHASA_INCLUDE_DIR) + +if(LHASA_FOUND) + set(LHASA_INCLUDE_DIRS ${LHASA_INCLUDE_DIR}) + + if(NOT LHASA_LIBRARIES) + set(LHASA_LIBRARIES ${LHASA_LIBRARIES}) + endif() + + if(NOT TARGET LHASA::liblhasa) + add_library(LHASA::liblhasa UNKNOWN IMPORTED) + set_target_properties(LHASA::liblhasa PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${LHASA_INCLUDE_DIRS}" + IMPORTED_LOCATION "${LHASA_LIBRARY}") + endif() +endif() + +mark_as_advanced(LHASA_INCLUDE_DIR LHASA_LIBRARY) diff --git a/configure.ac b/configure.ac index 64f9256f711..37e8fbbbc7f 100644 --- a/configure.ac +++ b/configure.ac @@ -95,6 +95,7 @@ EP_PKG_CHECK([FREETYPE],[freetype2],[Custom Font rendering.]) AS_IF([test "$with_freetype" = "yes"],[ EP_PKG_CHECK([HARFBUZZ],[harfbuzz],[Custom Font text shaping.]) ]) +EP_PKG_CHECK([LHASA],[liblhasa],[Support running games in lzh archives.]) AC_ARG_WITH([audio],[AS_HELP_STRING([--without-audio], [Disable audio support. @<:@default=on@:>@])]) AS_IF([test "x$with_audio" != "xno"],[ @@ -226,6 +227,7 @@ if test "yes" != "$silent"; then echo " -custom Font rendering (freetype2): $with_freetype" test "$with_freetype" = "yes" && \ echo " -custom Font text shaping (harfbuzz): $with_harfbuzz" + echo " -run games in lzh archives (lhasa): $with_lhasa" if test "$with_audio" = "no"; then echo "Audio support: no" diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 56a823317c4..15659538dd7 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -17,6 +17,7 @@ #include "filesystem.h" #include "filesystem_native.h" +#include "filesystem_lzh.h" #include "filesystem_zip.h" #include "filesystem_stream.h" #include "filefinder.h" @@ -122,7 +123,7 @@ FilesystemView Filesystem::Create(StringView path) const { } else { path_prefix += comp + "/"; auto sv = StringView(comp); - if (sv.ends_with(".zip") || sv.ends_with(".easyrpg")) { + if (sv.ends_with(".zip") || sv.ends_with(".easyrpg") || sv.ends_with(".lzh")) { path_prefix.pop_back(); handle_internal = true; } @@ -133,9 +134,12 @@ FilesystemView Filesystem::Create(StringView path) const { internal_path.pop_back(); } - auto filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + std::shared_ptr filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); if (!filesystem->IsValid()) { - return FilesystemView(); + filesystem = std::make_shared(path_prefix, Subtree(dir_of_file)); + if (!filesystem->IsValid()) { + return FilesystemView(); + } } if (!internal_path.empty()) { auto fs_view = filesystem->Create(internal_path); diff --git a/src/filesystem_lzh.cpp b/src/filesystem_lzh.cpp new file mode 100644 index 00000000000..fe2b17b1494 --- /dev/null +++ b/src/filesystem_lzh.cpp @@ -0,0 +1,390 @@ +/* + * 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 . + */ + +#ifdef HAVE_LHASA + +#include "filesystem_lzh.h" +#include "filefinder.h" +#include "output.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lhasa.h" + +constexpr uint32_t end_of_central_directory = 0x06054b50; +constexpr int32_t end_of_central_directory_size = 22; + +constexpr uint32_t central_directory_entry = 0x02014b50; +constexpr uint32_t local_header = 0x04034b50; +constexpr uint32_t local_header_size = 30; + +static std::string normalize_path(StringView path) { + if (path == "." || path == "/" || path == "") { + return ""; + }; + std::string inner_path = FileFinder::MakeCanonical(path, 1); + std::replace(inner_path.begin(), inner_path.end(), '\\', '/'); + if (inner_path.front() == '.') { + inner_path = inner_path.substr(1, inner_path.size() - 1); + } + if (inner_path.front() == '/') { + inner_path = inner_path.substr(1, inner_path.size() - 1); + } + return inner_path; +} + +static int vio_read_func(void* handle, void* buf, size_t buf_len) { + auto* f = reinterpret_cast(handle); + if (buf_len == 0) return 0; + return f->read(reinterpret_cast(buf), buf_len).gcount(); +} + +static int vio_skip_func(void* handle, size_t bytes) { + auto* f = reinterpret_cast(handle); + f->seekg(bytes, std::ios_base::cur); + return 1; +} + +static size_t vio_read_dec_func(void* buf, size_t buf_len, void* user_data) { + auto* f = reinterpret_cast(user_data); + if (buf_len == 0) return 0; + f->read(reinterpret_cast(buf), buf_len); + return f->gcount(); +} + +static LHAInputStreamType vio = { + vio_read_func, + vio_skip_func, + nullptr // close not supported by istream interface +}; + +LzhFilesystem::LzhFilesystem(std::string base_path, FilesystemView parent_fs, StringView enc) : + Filesystem(base_path, parent_fs) { + is = parent_fs.OpenInputStream(GetPath()); + if (!is) { + return; + } + + lha_is.reset(lha_input_stream_new(&vio, &is)); + + lha_reader.reset(lha_reader_new(lha_is.get())); + + if (!lha_reader) { + return; + } + + encoding = ToString(enc); + LHAFileHeader* header; + + LzhEntry entry; + std::vector paths; + + // Compressed data offset is manually calculated to reduce calls to tellg() + size_t last_offset = is.tellg(); + + // TODO: Encoding detection + + while ((header = lha_reader_next_file(lha_reader.get())) != nullptr) { + std::string filepath; + + if (!strcmp(header->compress_method, LHA_COMPRESS_TYPE_DIR)) { + last_offset += header->raw_data_len; + + filepath = header->path; + if (filepath.back() == '/') { + filepath.pop_back(); + } + std::cout << "DIR: " << filepath << "\n"; + paths.push_back(filepath); + } else { + entry.uncompressed_size = header->length; + entry.compressed_size = header->compressed_length; + entry.fileoffset = last_offset + header->raw_data_len; + last_offset = entry.fileoffset + entry.compressed_size; + + std::cout << entry.fileoffset << " | " << is.tellg() << " | " << entry.uncompressed_size << "\n"; + + entry.is_directory = false; + entry.compress_method = header->compress_method; + if (header->path != nullptr) { + filepath = header->path; + if (filepath.back() == '/') { + paths.push_back(filepath); + paths.back().pop_back(); + } else { + paths.push_back(filepath); + filepath += '/'; + } + } + filepath += header->filename; + std::cout << "FILE: " << filepath << "\n"; + + lzh_entries.emplace_back(filepath, entry); + } + + // Determine intermediate directories + for (;;) { + filepath = std::get<0>(FileFinder::GetPathAndFilename(filepath)); + if (filepath.empty()) { + break; + } + paths.push_back(filepath); + } + } + + /*if (encoding.empty()) { + zipfile.seekg(central_directory_offset); + std::stringstream filename_guess; + + // Guess the encoding first + int items = 0; + while (ReadCentralDirectoryEntry(zipfile, filepath, entry, is_utf8)) { + // Only consider Non-ASCII & Non-UTF8 for encoding detection + // Skip directories, files already contain the paths + if (is_utf8 || filepath.back() == '/' || Utils::StringIsAscii(filepath)) { + continue; + } + // Codepath will be only entered by Windows "compressed folder" ZIPs (uses local encoding) and + // 7zip (uses CP932 for Western European filenames) + + auto pos = filepath.find_last_of('/'); + if (pos == std::string::npos) { + filename_guess << filepath; + } else { + filename_guess << filepath.substr(pos + 1); + } + + ++items; + + if (items == 10) { + break; + } + } + + if (items == 0) { + // Only ASCII or UTF-8 flags set + encoding = "UTF-8"; + } else { + std::vector encodings = lcf::ReaderUtil::DetectEncodings(filename_guess.str()); + for (const auto &enc_ : encodings) { + std::string enc_test = lcf::ReaderUtil::Recode("\\", enc_); + if (enc_test.empty()) { + // Bad encoding + Output::Debug("Bad encoding: {}. Trying next.", enc_); + continue; + } + encoding = enc_; + break; + } + } + Output::Debug("Detected ZIP encoding: {}", encoding); + }*/ +/* + zipfile.clear(); + zipfile.seekg(central_directory_offset); + + std::vector paths; + while (ReadCentralDirectoryEntry(zipfile, filepath, entry, is_utf8)) { + if (is_utf8 || enc_is_utf8 || Utils::StringIsAscii(filepath)) { + // No reencoding necessary + filepath_cp437.clear(); + } else { + // also store CP437 to ensure files inside 7zip zip archives are found + filepath_cp437 = lcf::ReaderUtil::Recode(filepath, "437"); + filepath = lcf::ReaderUtil::Recode(filepath, encoding); + } + + // Workaround ZIP archives containing invalid "\" paths created by .net or Powershell + std::replace(filepath_cp437.begin(), filepath_cp437.end(), '\\', '/'); + std::replace(filepath.begin(), filepath.end(), '\\', '/'); + + // check if the entry is an directory or not (indicated by trailing /) + // this will fail when the (game) directory has cp437, but the users can rename it before + if (filepath.back() == '/') { + filepath = filepath.substr(0, filepath.size() - 1); + + // Determine intermediate directories + while (!filepath.empty()) { + paths.push_back(filepath); + filepath = std::get<0>(FileFinder::GetPathAndFilename(filepath)); + } + } else { + lzh_entries.emplace_back(filepath, entry); + if (!filepath_cp437.empty()) { + lzh_entries_cp437.emplace_back(filepath_cp437, entry); + } + + // Determine intermediate directories + for (;;) { + filepath = std::get<0>(FileFinder::GetPathAndFilename(filepath)); + if (filepath.empty()) { + break; + } + paths.push_back(filepath); + } + } + } + }*/ + + // Build directories + entry = {}; + entry.is_directory = true; + + // add root path + paths.emplace_back(""); + + std::sort(paths.begin(), paths.end()); + auto paths_del_it = std::unique(paths.begin(), paths.end()); + paths.erase(paths_del_it, paths.end()); + for (const auto& e : paths) { + lzh_entries.emplace_back(e, entry); + } + + // entries can be duplicated in the lzh archive, e.g. when creating a game disk the RTP is embedded, followed by + // the game entries. Use a stable sort to preserve this order. + std::stable_sort(lzh_entries.begin(), lzh_entries.end(), [](auto& a, auto& b) { + return a.first < b.first; + }); + + // Then remove all duplicates but keep the last + auto entries_del_it = std::unique(lzh_entries.rbegin(), lzh_entries.rend(), [](auto& a, auto& b) { + return a.first < b.first; + }); + lzh_entries.erase(lzh_entries.begin(), entries_del_it.base()); +} + +bool LzhFilesystem::IsFile(StringView path) const { + std::string path_normalized = normalize_path(path); + auto entry = Find(path); + if (entry) { + return !entry->is_directory; + } + return false; +} + +bool LzhFilesystem::IsDirectory(StringView path, bool) const { + std::string path_normalized = normalize_path(path); + auto entry = Find(path); + if (entry) { + return entry->is_directory; + } + return false; +} + +bool LzhFilesystem::Exists(StringView path) const { + std::string path_normalized = normalize_path(path); + auto entry = Find(path); + return entry != nullptr; +} + +int64_t LzhFilesystem::GetFilesize(StringView path) const { + std::string path_normalized = normalize_path(path); + auto entry = Find(path); + if (entry) { + return entry->uncompressed_size; + } + return 0; +} + +std::streambuf* LzhFilesystem::CreateInputStreambuffer(StringView path, std::ios_base::openmode) const { + std::string path_normalized = normalize_path(path); + auto entry = Find(path); + if (entry && !entry->is_directory) { + // Determine compression method + auto* decoder_type = lha_decoder_for_name(const_cast(entry->compress_method.c_str())); + + if (!decoder_type) { + // TODO Error + return nullptr; + } + + // Seek to the compressed data + is.clear(); + is.seekg(entry->fileoffset, std::ios_base::beg); + + // Create a suitable decoder for the compression method + std::unique_ptr decoder; + decoder.reset(lha_decoder_new(decoder_type, vio_read_dec_func, &is, entry->uncompressed_size)); + + // Decompress + auto dec_buf = std::vector(entry->uncompressed_size); + size_t res = lha_decoder_read(decoder.get(), dec_buf.data(), dec_buf.size()); + + // TODO: Error handling + + return new Filesystem_Stream::InputMemoryStreamBuf(std::move(dec_buf)); + } + + return nullptr; +} + +bool LzhFilesystem::GetDirectoryContent(StringView path, std::vector& entries) const { + if (!IsDirectory(path, false)) { + return false; + } + + std::string path_normalized = normalize_path(path); + if (!path_normalized.empty() && path_normalized.back() != '/') { + path_normalized += "/"; + } + + auto check = [&](auto& it) { + if (StringView(it.first).starts_with(path_normalized) && + it.first.substr(path_normalized.size(), it.first.size() - path_normalized.size()).find_last_of('/') == std::string::npos) { + // Everything that starts with the path but isn't the path and does contain no slash + auto filename = it.first.substr(path_normalized.size(), it.first.size() - path_normalized.size()); + if (filename.empty()) { + return; + } + + entries.emplace_back( + it.first.substr(path_normalized.size(), it.first.size() - path_normalized.size()), + it.second.is_directory ? DirectoryTree::FileType::Directory : DirectoryTree::FileType::Regular); + } + }; + + for (const auto& it : lzh_entries) { + check(it); + } + + return true; +} + +const LzhFilesystem::LzhEntry* LzhFilesystem::Find(StringView what) const { + auto it = std::lower_bound(lzh_entries.begin(), lzh_entries.end(), what, [](const auto& e, const auto& w) { + return e.first < w; + }); + if (it != lzh_entries.end() && it->first == what) { + return &it->second; + } + + return nullptr; +} + +std::string LzhFilesystem::Describe() const { + return fmt::format("[LZH] {} ({})", GetPath(), encoding); +} + +#endif diff --git a/src/filesystem_lzh.h b/src/filesystem_lzh.h new file mode 100644 index 00000000000..0f5eaf352a3 --- /dev/null +++ b/src/filesystem_lzh.h @@ -0,0 +1,98 @@ +/* + * 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_LZH_H +#define EP_FILESYSTEM_LZH_H + +#include "system.h" + +#ifdef HAVE_LHASA + +#include "filesystem.h" +#include "filesystem_stream.h" +#include +#include +#include +#include + +#include + +/** + * A virtual filesystem that allows file/directory operations inside a LZH archive. + */ +class LzhFilesystem : public Filesystem { +public: + /** + * Initializes a filesystem inside the given LZH File + * + * @param base_path Path passed to parent_fs to open the LZH file + * @param parent_fs Filesystem used to create handles on the LZH file + * @param encoding Encoding to use, use empty string for autodetection + */ + LzhFilesystem(std::string base_path, FilesystemView parent_fs, StringView encoding = ""); + +protected: + /** + * 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; + std::streambuf* CreateInputStreambuffer(StringView path, std::ios_base::openmode mode) const override; + bool GetDirectoryContent(StringView path, std::vector& entries) const override; + std::string Describe() const override; + /** @} */ + +private: + struct LzhEntry { + size_t compressed_size; + size_t uncompressed_size; + size_t fileoffset; + std::string compress_method; + bool is_directory; + }; + + const LzhEntry* Find(StringView what) const; + + std::vector> lzh_entries; + std::string encoding; + mutable std::vector filename_buffer; + + struct LhasaDeleter { + void operator()(LHAInputStream* o) const { + lha_input_stream_free(o); + } + + void operator()(LHAReader* o) const { + lha_reader_free(o); + } + + void operator()(LHADecoder* o) const { + lha_decoder_free(o); + } + }; + + mutable Filesystem_Stream::InputStream is; + mutable std::unique_ptr lha_is; + mutable std::unique_ptr lha_reader; +}; + +#endif + +#endif diff --git a/src/filesystem_zip.cpp b/src/filesystem_zip.cpp index 7c0e4cf6742..68918526d92 100644 --- a/src/filesystem_zip.cpp +++ b/src/filesystem_zip.cpp @@ -184,7 +184,7 @@ ZipFilesystem::ZipFilesystem(std::string base_path, FilesystemView parent_fs, St return a.first < b.first; }); } else { - Output::Warning("ZipFS: {} is not a valid archive", GetPath()); + Output::Debug("ZipFS: {} is not a valid archive", GetPath()); } } diff --git a/src/window_gamelist.cpp b/src/window_gamelist.cpp index eec944888be..8f0ad35878e 100644 --- a/src/window_gamelist.cpp +++ b/src/window_gamelist.cpp @@ -55,7 +55,7 @@ bool Window_GameList::Refresh(FilesystemView filesystem_base, bool show_dotdot) } if (dir.second.type == DirectoryTree::FileType::Regular) { auto sv = StringView(dir.second.name); - if (sv.ends_with(".zip") || sv.ends_with(".easyrpg")) { + if (sv.ends_with(".zip") || sv.ends_with(".easyrpg") || sv.ends_with(".lzh")) { game_directories.emplace_back(dir.second.name); } } else if (dir.second.type == DirectoryTree::FileType::Directory) {