mirror of
https://github.com/open-goal/jak-project.git
synced 2024-10-20 21:27:52 -04:00
630388229e
My mistake -- testing focused too much on preserving the existing behaviour I clearly forgot to make sure the new stuff worked properly. Just had to early out and not modify the args if they were in the new format.
612 lines
17 KiB
C++
612 lines
17 KiB
C++
/*!
|
|
* @file FileUtil.cpp
|
|
* Utility functions for reading and writing files.
|
|
*/
|
|
|
|
#include "FileUtil.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio> /* defines FILENAME_MAX */
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include "BinaryWriter.h"
|
|
|
|
#include "common/common_types.h"
|
|
#include "common/util/BinaryReader.h"
|
|
#include "common/util/unicode_util.h"
|
|
|
|
// This disables the use of PCLMULQDQ which is probably ok, but let's just be safe and disable it
|
|
// because nobody will care if png compression is 10% slower.
|
|
#define FPNG_NO_SSE 1
|
|
#include "third-party/fmt/core.h"
|
|
#include "third-party/fpng/fpng.cpp"
|
|
#include "third-party/fpng/fpng.h"
|
|
#include "third-party/lzokay/lzokay.hpp"
|
|
|
|
#ifdef _WIN32
|
|
#define NOMINMAX
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#else
|
|
#include <cstring>
|
|
#include <unistd.h>
|
|
#endif
|
|
#include "common/log/log.h"
|
|
#include "common/util/Assert.h"
|
|
|
|
namespace file_util {
|
|
fs::path get_user_home_dir() {
|
|
#ifdef _WIN32
|
|
// NOTE - on older systems, this may case issues if it cannot be found!
|
|
std::string home_dir = get_env("USERPROFILE");
|
|
return fs::path(home_dir);
|
|
#else
|
|
std::string home_dir = get_env("HOME");
|
|
return fs::path(home_dir);
|
|
#endif
|
|
}
|
|
|
|
fs::path get_user_config_dir() {
|
|
fs::path config_base_path;
|
|
#ifdef _WIN32
|
|
auto config_base_dir = get_env("APPDATA");
|
|
config_base_path = fs::path(config_base_dir);
|
|
#elif __linux
|
|
// Docs - https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
// Prefer XDG_CONFIG_HOME if available
|
|
auto config_base_dir = get_env("XDG_CONFIG_HOME");
|
|
if (config_base_dir.empty()) {
|
|
config_base_path = get_user_home_dir() / ".config";
|
|
} else {
|
|
config_base_path = fs::path(config_base_dir);
|
|
}
|
|
#endif
|
|
return config_base_path / "OpenGOAL";
|
|
}
|
|
|
|
fs::path get_user_settings_dir(GameVersion game_version) {
|
|
auto game_version_name = game_version_names[game_version];
|
|
return get_user_config_dir() / game_version_name / "settings";
|
|
}
|
|
|
|
fs::path get_user_memcard_dir(GameVersion game_version) {
|
|
auto game_version_name = game_version_names[game_version];
|
|
return get_user_config_dir() / game_version_name / "saves";
|
|
}
|
|
|
|
fs::path get_user_misc_dir(GameVersion game_version) {
|
|
auto game_version_name = game_version_names[game_version];
|
|
return get_user_config_dir() / game_version_name / "misc";
|
|
}
|
|
|
|
struct {
|
|
bool initialized = false;
|
|
fs::path path_to_data;
|
|
} gFilePathInfo;
|
|
|
|
/*!
|
|
* Get the path to the current executable.
|
|
*/
|
|
std::string get_current_executable_path() {
|
|
#ifdef _WIN32
|
|
// NOTE - MAX_PATH is kinda wrong here as you can have a path longer than 260 in windows
|
|
wchar_t path[MAX_PATH];
|
|
GetModuleFileNameW(NULL, path, MAX_PATH);
|
|
std::string file_path = wide_string_to_utf8_string(path);
|
|
if (file_path.rfind("\\\\?\\", 0) == 0) {
|
|
return file_path.substr(4);
|
|
}
|
|
return file_path;
|
|
#else
|
|
// do Linux stuff
|
|
char buffer[FILENAME_MAX + 1];
|
|
auto len = readlink("/proc/self/exe", buffer,
|
|
FILENAME_MAX); // /proc/self acts like a "virtual folder" containing
|
|
// information about the current process
|
|
buffer[len] = '\0';
|
|
return std::string(buffer);
|
|
#endif
|
|
}
|
|
|
|
/*!
|
|
* See if the current executable is somewhere in jak-project/. If so, return the path to jak-project
|
|
*/
|
|
std::optional<std::string> try_get_jak_project_path() {
|
|
std::string my_path = get_current_executable_path();
|
|
|
|
std::string::size_type pos =
|
|
std::string(my_path).rfind("jak-project"); // Strip file path down to /jak-project/ directory
|
|
if (pos == std::string::npos) {
|
|
return {};
|
|
}
|
|
|
|
return std::make_optional(std::string(my_path).substr(
|
|
0, pos + 11)); // + 12 to include "/jak-project" in the returned filepath
|
|
}
|
|
|
|
std::optional<fs::path> try_get_data_dir() {
|
|
fs::path my_path = get_current_executable_path();
|
|
lg::info("Current executable directory - {}", my_path.string());
|
|
auto data_dir = my_path.parent_path() / "data";
|
|
if (fs::exists(data_dir) && fs::is_directory(data_dir)) {
|
|
return std::make_optional(data_dir);
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
bool setup_project_path(std::optional<fs::path> project_path_override) {
|
|
if (gFilePathInfo.initialized) {
|
|
return true;
|
|
}
|
|
|
|
if (project_path_override) {
|
|
gFilePathInfo.path_to_data = *project_path_override;
|
|
gFilePathInfo.initialized = true;
|
|
lg::info("Using explicitly set project path: {}", project_path_override->string());
|
|
return true;
|
|
}
|
|
|
|
auto data_path = try_get_data_dir();
|
|
if (data_path) {
|
|
gFilePathInfo.path_to_data = *data_path;
|
|
gFilePathInfo.initialized = true;
|
|
lg::info("Using data path: {}", data_path->string());
|
|
return true;
|
|
}
|
|
|
|
auto development_repo_path = try_get_jak_project_path();
|
|
if (development_repo_path) {
|
|
gFilePathInfo.path_to_data = *development_repo_path;
|
|
gFilePathInfo.initialized = true;
|
|
lg::info("Using development repo path: {}", *development_repo_path);
|
|
return true;
|
|
}
|
|
|
|
lg::error("Failed to initialize project path.");
|
|
return false;
|
|
}
|
|
|
|
fs::path get_jak_project_dir() {
|
|
ASSERT(gFilePathInfo.initialized);
|
|
return gFilePathInfo.path_to_data;
|
|
}
|
|
|
|
std::string get_file_path(const std::vector<std::string>& input) {
|
|
// TODO - clean this behaviour up, it causes unexpected behaviour when working with files
|
|
// the project path should be explicitly provided by whatever if needed
|
|
// TEMP HACK
|
|
// - if the provided path is absolute, don't add the project path
|
|
if (input.size() == 1 && fs::path(input.at(0)).is_absolute()) {
|
|
return input.at(0);
|
|
}
|
|
|
|
auto current_path = file_util::get_jak_project_dir();
|
|
for (auto& str : input) {
|
|
current_path /= str;
|
|
}
|
|
|
|
return current_path.string();
|
|
}
|
|
|
|
bool create_dir_if_needed(const fs::path& path) {
|
|
if (!fs::is_directory(path)) {
|
|
fs::create_directories(path);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool create_dir_if_needed_for_file(const std::string& path) {
|
|
return create_dir_if_needed_for_file(fs::path(path));
|
|
}
|
|
|
|
bool create_dir_if_needed_for_file(const fs::path& path) {
|
|
return fs::create_directories(path.parent_path());
|
|
}
|
|
|
|
void write_binary_file(const fs::path& name, const void* data, size_t size) {
|
|
FILE* fp = file_util::open_file(name.string().c_str(), "wb");
|
|
if (!fp) {
|
|
throw std::runtime_error("couldn't open file " + name.string());
|
|
}
|
|
|
|
if (size == 0) {
|
|
// nothing to write, just 'touch' the file
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
|
|
if (fwrite(data, size, 1, fp) != 1) {
|
|
fclose(fp);
|
|
throw std::runtime_error("couldn't write file " + name.string());
|
|
}
|
|
|
|
fclose(fp);
|
|
}
|
|
|
|
void write_binary_file(const std::string& name, const void* data, size_t size) {
|
|
write_binary_file(fs::path(name), data, size);
|
|
}
|
|
|
|
void write_rgba_png(const fs::path& name, void* data, int w, int h) {
|
|
auto flags = 0;
|
|
|
|
auto ok = fpng::fpng_encode_image_to_file(name.string().c_str(), data, w, h, 4, flags);
|
|
|
|
if (!ok) {
|
|
throw std::runtime_error("couldn't save png file " + name.string());
|
|
}
|
|
}
|
|
|
|
void write_text_file(const std::string& file_name, const std::string& text) {
|
|
write_text_file(fs::path(file_name), text);
|
|
}
|
|
|
|
void write_text_file(const fs::path& file_name, const std::string& text) {
|
|
FILE* fp = file_util::open_file(file_name.string().c_str(), "w");
|
|
if (!fp) {
|
|
lg::error("Failed to fopen {}\n", file_name.string());
|
|
throw std::runtime_error("Failed to open file");
|
|
}
|
|
fprintf(fp, "%s\n", text.c_str());
|
|
fclose(fp);
|
|
}
|
|
std::vector<uint8_t> read_binary_file(const std::string& filename) {
|
|
return read_binary_file(fs::path(filename));
|
|
}
|
|
|
|
std::vector<uint8_t> read_binary_file(const fs::path& path) {
|
|
// make sure file exists and isn't a directory
|
|
|
|
auto status = fs::status(path);
|
|
|
|
if (!fs::exists(status)) {
|
|
throw std::runtime_error(
|
|
fmt::format("File {} cannot be opened: does not exist.", path.string()));
|
|
}
|
|
|
|
if (status.type() != fs::file_type::regular && status.type() != fs::file_type::symlink) {
|
|
throw std::runtime_error(
|
|
fmt::format("File {} cannot be opened: not a regular file or symlink.", path.string()));
|
|
}
|
|
|
|
auto fp = file_util::open_file(path.string().c_str(), "rb");
|
|
if (!fp)
|
|
throw std::runtime_error("File " + path.string() +
|
|
" cannot be opened: " + std::string(strerror(errno)));
|
|
fseek(fp, 0, SEEK_END);
|
|
auto len = ftell(fp);
|
|
if (len == 0) {
|
|
fclose(fp);
|
|
return {};
|
|
}
|
|
rewind(fp);
|
|
|
|
std::vector<uint8_t> data;
|
|
data.resize(len);
|
|
|
|
if (fread(data.data(), len, 1, fp) != 1) {
|
|
fclose(fp);
|
|
throw std::runtime_error("File " + path.string() + " cannot be read");
|
|
}
|
|
fclose(fp);
|
|
|
|
return data;
|
|
}
|
|
|
|
std::string read_text_file(const fs::path& path) {
|
|
fs::ifstream file(path);
|
|
if (!file.good()) {
|
|
throw std::runtime_error("couldn't open " + path.string());
|
|
}
|
|
std::stringstream ss;
|
|
ss << file.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
std::string read_text_file(const std::string& path) {
|
|
return read_text_file(fs::path(path));
|
|
}
|
|
|
|
bool is_printable_char(char c) {
|
|
return c >= ' ' && c <= '~';
|
|
}
|
|
|
|
std::string combine_path(const std::string& parent, const std::string& child) {
|
|
return parent + "/" + child;
|
|
}
|
|
|
|
bool file_exists(const std::string& path) {
|
|
return fs::exists(path);
|
|
}
|
|
|
|
std::string base_name(const std::string& filename) {
|
|
size_t pos = 0;
|
|
ASSERT(!filename.empty());
|
|
for (size_t i = filename.size() - 1; i-- > 0;) {
|
|
if (filename.at(i) == '/' || filename.at(i) == '\\') {
|
|
pos = (i + 1);
|
|
break;
|
|
}
|
|
}
|
|
return filename.substr(pos);
|
|
}
|
|
|
|
std::string base_name_no_ext(const std::string& filename) {
|
|
size_t pos = 0;
|
|
ASSERT(!filename.empty());
|
|
for (size_t i = filename.size() - 1; i-- > 0;) {
|
|
if (filename.at(i) == '/' || filename.at(i) == '\\') {
|
|
pos = (i + 1);
|
|
break;
|
|
}
|
|
}
|
|
std::string file_name = filename.substr(pos);
|
|
return file_name.substr(0, file_name.find_last_of('.'));
|
|
;
|
|
}
|
|
|
|
std::string split_path_at(const fs::path& path, const std::vector<std::string>& folders) {
|
|
std::string split_str = "";
|
|
for (const auto& folder : folders) {
|
|
#ifdef _WIN32
|
|
split_str += folder + "\\";
|
|
#else
|
|
split_str += folder + "/";
|
|
#endif
|
|
}
|
|
const auto& path_str = path.u8string();
|
|
return path_str.substr(path_str.find(split_str) + split_str.length());
|
|
}
|
|
|
|
std::string convert_to_unix_path_separators(const std::string& path) {
|
|
#ifdef _WIN32
|
|
std::string copy = path;
|
|
std::replace(copy.begin(), copy.end(), '\\', '/');
|
|
return copy;
|
|
#else
|
|
return path;
|
|
#endif
|
|
}
|
|
|
|
void ISONameFromAnimationName(char* dst, const char* src) {
|
|
// The Animation Name is a bunch of words separated by dashes
|
|
|
|
// copy first two chars of the first word exactly
|
|
dst[0] = src[0];
|
|
dst[1] = src[1];
|
|
s32 i = 2; // 2 chars added to dst.
|
|
|
|
// skip ahead to the first dash (or \0 if there's no dashes)
|
|
const char* src_ptr = src;
|
|
while (*src_ptr && *src_ptr != '-') {
|
|
src_ptr++;
|
|
}
|
|
|
|
// the points to the next dash (or \0 if there's none).
|
|
const char* next_ptr = src_ptr;
|
|
if (*src_ptr) {
|
|
// loop over words (next_ptr points to dash before word, i counts chars in dest)
|
|
while (src_ptr = next_ptr + 1, i < 8) {
|
|
// scan next_ptr forward to next dash
|
|
next_ptr = src_ptr;
|
|
while (*next_ptr && *next_ptr != '-') {
|
|
next_ptr++;
|
|
}
|
|
|
|
// there's no next word, so break (the current word will be handled there)
|
|
if (!*next_ptr)
|
|
break;
|
|
|
|
// add a char for the current word:
|
|
char char_to_add;
|
|
if (next_ptr[-1] < '0' || next_ptr[-1] > '9') {
|
|
// word doesn't end in a number.
|
|
|
|
// some special case words map to special letters (likely to avoid animation name conflicts)
|
|
if (next_ptr - src_ptr == 10 && !memcmp(src_ptr, "resolution", 10)) {
|
|
char_to_add = 'z';
|
|
} else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "accept", 6)) {
|
|
char_to_add = 'y';
|
|
} else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "reject", 6)) {
|
|
char_to_add = 'n';
|
|
} else {
|
|
// not a special case, just take the first letter.
|
|
char_to_add = *src_ptr;
|
|
}
|
|
} else {
|
|
// the current word ends in a number, just use this number (I think usually the whole word
|
|
// is just a number)
|
|
char_to_add = next_ptr[-1];
|
|
}
|
|
|
|
dst[i++] = char_to_add;
|
|
}
|
|
|
|
// here we ran out of room in dest, or words in source.
|
|
// if there's still room in dest and chars in source, just add them
|
|
while (*src_ptr && (i < 8)) {
|
|
dst[i] = *src_ptr;
|
|
src_ptr++;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// pad with spaces (for ISO Name)
|
|
while (i < 8) {
|
|
dst[i++] = ' ';
|
|
}
|
|
|
|
// upper case
|
|
for (i = 0; i < 8; i++) {
|
|
if (dst[i] >= 'a' && dst[i] <= 'z') {
|
|
dst[i] -= 0x20;
|
|
}
|
|
}
|
|
|
|
// append file extension
|
|
strcpy(dst + 8, "STR");
|
|
}
|
|
|
|
void MakeISOName(char* dst, const char* src) {
|
|
int i = 0;
|
|
const char* src_ptr = src;
|
|
char* dst_ptr = dst;
|
|
|
|
// copy name and upper case
|
|
while ((i < 8) && (*src_ptr) && (*src_ptr != '.')) {
|
|
char c = *src_ptr;
|
|
src_ptr++;
|
|
if (('`' < c) && (c < '{')) { // lower case
|
|
c -= 0x20;
|
|
}
|
|
*dst_ptr = c;
|
|
dst_ptr++;
|
|
i++;
|
|
}
|
|
|
|
// pad out name with spaces
|
|
while (i < 8) {
|
|
*dst_ptr = ' ';
|
|
dst_ptr++;
|
|
i++;
|
|
}
|
|
|
|
// increment past period
|
|
if (*src_ptr == '.')
|
|
src_ptr++;
|
|
|
|
// same for extension
|
|
while (i < 11 && (*src_ptr)) {
|
|
char c = *src_ptr;
|
|
src_ptr++;
|
|
if (('`' < c) && (c < '{')) { // lower case
|
|
c -= 0x20;
|
|
}
|
|
*dst_ptr = c;
|
|
dst_ptr++;
|
|
i++;
|
|
}
|
|
|
|
while (i < 11) {
|
|
*dst_ptr = ' ';
|
|
dst_ptr++;
|
|
i++;
|
|
}
|
|
*dst_ptr = 0;
|
|
}
|
|
|
|
void assert_file_exists(const char* path, const char* error_message) {
|
|
if (!fs::exists(path)) {
|
|
ASSERT_MSG(false, fmt::format("File {} was not found: {}", path, error_message));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Check if the given DGO header (or entire file) is compressed.
|
|
*/
|
|
bool dgo_header_is_compressed(const std::vector<u8>& data) {
|
|
const char compressed_header[] = "oZlB";
|
|
bool is_compressed = true;
|
|
for (int i = 0; i < 4; i++) {
|
|
if (compressed_header[i] != data.at(i)) {
|
|
is_compressed = false;
|
|
}
|
|
}
|
|
return is_compressed;
|
|
}
|
|
|
|
/*!
|
|
* Decompress a DGO. Resulting data will start at the DGO header.
|
|
*/
|
|
std::vector<u8> decompress_dgo(const std::vector<u8>& data_in) {
|
|
constexpr int MAX_CHUNK_SIZE = 0x8000;
|
|
BinaryReader compressed_reader(data_in);
|
|
// seek past oZlB
|
|
compressed_reader.ffwd(4);
|
|
std::size_t decompressed_size = compressed_reader.read<uint32_t>();
|
|
std::vector<uint8_t> decompressed_data;
|
|
decompressed_data.resize(decompressed_size);
|
|
size_t output_offset = 0;
|
|
while (true) {
|
|
// seek past alignment bytes and read the next chunk size
|
|
uint32_t chunk_size = 0;
|
|
while (!chunk_size) {
|
|
chunk_size = compressed_reader.read<uint32_t>();
|
|
}
|
|
|
|
if (chunk_size < MAX_CHUNK_SIZE) {
|
|
std::size_t bytes_written = 0;
|
|
lzokay::EResult ok = lzokay::decompress(
|
|
compressed_reader.here(), chunk_size, decompressed_data.data() + output_offset,
|
|
decompressed_data.size() - output_offset, bytes_written);
|
|
ASSERT(ok == lzokay::EResult::Success);
|
|
compressed_reader.ffwd(chunk_size);
|
|
output_offset += bytes_written;
|
|
} else {
|
|
// nope - sometimes chunk_size is bigger than MAX, but we should still use max.
|
|
// ASSERT(chunk_size == MAX_CHUNK_SIZE);
|
|
memcpy(decompressed_data.data() + output_offset, compressed_reader.here(), MAX_CHUNK_SIZE);
|
|
compressed_reader.ffwd(MAX_CHUNK_SIZE);
|
|
output_offset += MAX_CHUNK_SIZE;
|
|
}
|
|
|
|
if (output_offset >= decompressed_size)
|
|
break;
|
|
while (compressed_reader.get_seek() % 4) {
|
|
compressed_reader.ffwd(1);
|
|
}
|
|
}
|
|
|
|
return decompressed_data;
|
|
}
|
|
|
|
FILE* open_file(const fs::path& path, const std::string& mode) {
|
|
#ifdef _WIN32
|
|
return _wfopen(path.wstring().c_str(), std::wstring(mode.begin(), mode.end()).c_str());
|
|
#else
|
|
return fopen(path.string().c_str(), mode.c_str());
|
|
#endif
|
|
}
|
|
|
|
std::vector<fs::path> find_files_recursively(const fs::path& base_dir, const std::regex& pattern) {
|
|
std::vector<fs::path> files = {};
|
|
for (auto& p : fs::recursive_directory_iterator(base_dir)) {
|
|
if (p.is_regular_file()) {
|
|
if (std::regex_match(p.path().filename().string(), pattern)) {
|
|
files.push_back(p.path());
|
|
}
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
std::vector<fs::path> find_directories_in_dir(const fs::path& base_dir) {
|
|
std::vector<fs::path> dirs = {};
|
|
for (auto& p : fs::recursive_directory_iterator(base_dir)) {
|
|
if (p.is_directory()) {
|
|
dirs.push_back(p.path());
|
|
}
|
|
}
|
|
return dirs;
|
|
}
|
|
|
|
void copy_file(const fs::path& src, const fs::path& dst) {
|
|
// Check that the src path exists
|
|
if (!fs::exists(src)) {
|
|
throw std::runtime_error(fmt::format("Cannot copy '{}', path does not exist", src.string()));
|
|
}
|
|
// Ensure the directory can be copied into
|
|
if (!fs::exists(dst.parent_path()) && !create_dir_if_needed_for_file(dst)) {
|
|
throw std::runtime_error(fmt::format(
|
|
"Cannot copy '{}', couldn't make directory to copy into '{}'", src.string(), dst.string()));
|
|
}
|
|
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
|
|
}
|
|
|
|
} // namespace file_util
|