mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
A mod archive numbered 65534 or 65535 is placed under a free index, so no real placement is read as a placeholder. carry() records every payload in one list (settle() still writes each once per install chunk) and keeps one archive index, as write() does.
152 lines
7.0 KiB
C++
152 lines
7.0 KiB
C++
#include "mod_merge_internal.h"
|
|
#include "Engine/Core/Platform/path_text.h"
|
|
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
|
|
namespace dingosdk::mods::detail {
|
|
|
|
bool unshift(const ArchivePlacement& placement, std::string_view directory, std::uint16_t& archive,
|
|
std::uint32_t& offset) {
|
|
const ArchivePlacement::Spot* block{};
|
|
std::uint16_t origin{};
|
|
for (const auto& [from, spot] : placement.at) {
|
|
if (from.first != directory || spot.archive != archive || spot.offset > offset) continue;
|
|
// Blocks are appended in archive order, so of two that start at the same
|
|
// byte the earlier one is empty and the offset is in the later.
|
|
if (!block || spot.offset >= block->offset) { block = &spot; origin = from.second; }
|
|
}
|
|
if (!block) return false;
|
|
archive = origin;
|
|
offset = static_cast<std::uint32_t>(offset - block->offset);
|
|
return true;
|
|
}
|
|
|
|
CasStore::CasStore(fs::path baseRoot, fs::path output, const native_db::Node& layout)
|
|
: GameArchives(std::move(baseRoot), layout), output_(std::move(output)) {
|
|
keep_open();
|
|
}
|
|
|
|
fs::path CasStore::archive_path(std::uint32_t installChunk, std::uint16_t archive) const {
|
|
return output_ / L"Win32" / fs::path(directory(installChunk)) / fs::path(archive_file(archive));
|
|
}
|
|
|
|
std::uint64_t CasStore::append(const fs::path& path, std::span<const std::byte> encoded) {
|
|
auto& offset = offsets_[path.wstring()];
|
|
if (!offset) {
|
|
fs::create_directories(path.parent_path());
|
|
forget(path); // unshare may put a private copy in its place
|
|
unshare(path);
|
|
std::error_code error;
|
|
const auto existing = fs::file_size(path, error);
|
|
if (!error) offset = existing;
|
|
}
|
|
// A placement holds a 32-bit offset; one past it would wrap round and
|
|
// point the game at some other payload.
|
|
if (offset + encoded.size() > std::numeric_limits<std::uint32_t>::max())
|
|
throw std::runtime_error("The merged patch's own archive " + path_utf8(path) +
|
|
" is full (4 GB); restart Skate so it is built again");
|
|
std::ofstream out(path, std::ios::binary | std::ios::app);
|
|
if (!out || !out.write(reinterpret_cast<const char*>(encoded.data()),
|
|
static_cast<std::streamsize>(encoded.size())))
|
|
throw std::runtime_error("Cannot append to " + path_utf8(path));
|
|
const auto start = offset;
|
|
offset += encoded.size();
|
|
return start;
|
|
}
|
|
|
|
fb::BundleFileInfo CasStore::write(std::uint32_t installChunk, std::uint16_t archive,
|
|
std::span<const std::byte> encoded) {
|
|
const auto path = archive_path(installChunk, archive);
|
|
if (waiting_) {
|
|
// A placement that waits names no archive, so there can be only the one to settle into.
|
|
if (placeholder(archive) || (archive_ && *archive_ != archive))
|
|
throw std::logic_error("A waiting store writes to one archive index");
|
|
archive_ = archive;
|
|
auto& held = held_[path.wstring()].bytes;
|
|
if (held.size() + encoded.size() > std::numeric_limits<std::uint32_t>::max())
|
|
throw std::runtime_error("The merged patch's own archive " + path_utf8(path) +
|
|
" is full (4 GB); restart Skate so it is built again");
|
|
const fb::BundleFileInfo info{{true, installChunk, waiting_archive}, static_cast<std::uint32_t>(held.size()),
|
|
static_cast<std::uint32_t>(encoded.size())};
|
|
held.insert(held.end(), encoded.begin(), encoded.end());
|
|
return info;
|
|
}
|
|
const auto start = append(path, encoded);
|
|
return {{true, installChunk, archive}, static_cast<std::uint32_t>(start), static_cast<std::uint32_t>(encoded.size())};
|
|
}
|
|
|
|
CasStore CasStore::waiting() const {
|
|
auto copy = *this;
|
|
copy.waiting_ = true;
|
|
copy.archive_.reset();
|
|
copy.held_.clear();
|
|
copy.offsets_.clear();
|
|
copy.carried_.clear();
|
|
copy.carriedAt_.clear();
|
|
return copy;
|
|
}
|
|
|
|
fb::BundleFileInfo CasStore::carry(std::uint32_t installChunk, std::uint16_t archive, std::span<const std::byte> encoded) {
|
|
if (!waiting_) throw std::logic_error("Only a waiting store carries payloads");
|
|
if (placeholder(archive) || (archive_ && *archive_ != archive))
|
|
throw std::logic_error("A waiting store writes to one archive index");
|
|
// An install chunk no package names fails here, for this payload alone.
|
|
(void)directory(installChunk);
|
|
archive_ = archive;
|
|
carried_.push_back({installChunk, sha1_of(encoded), encoded, {}});
|
|
return {{true, installChunk, carried_archive}, static_cast<std::uint32_t>(carried_.size() - 1),
|
|
static_cast<std::uint32_t>(encoded.size())};
|
|
}
|
|
|
|
void CasStore::settle(CasStore& waiting) {
|
|
// In path order: which file comes first makes no difference to where anything lands.
|
|
for (auto& [path, held] : waiting.held_) {
|
|
held.at = append(fs::path(path), held.bytes);
|
|
held.bytes = {};
|
|
}
|
|
// Then the carried assets, each written once per install chunk.
|
|
for (auto& carried : waiting.carried_) {
|
|
const std::pair key{carried.installChunk, carried.sha1};
|
|
auto found = carriedAt_.find(key);
|
|
if (found == carriedAt_.end())
|
|
found = carriedAt_.emplace(key, write(carried.installChunk, *waiting.archive_, carried.bytes)).first;
|
|
carried.at = found->second;
|
|
}
|
|
}
|
|
|
|
bool CasStore::settled(fb::CasIdentifier& location, std::uint32_t& offset) const {
|
|
if (!placeholder(location)) return false;
|
|
if (location.archive == carried_archive) {
|
|
if (offset >= carried_.size()) throw std::logic_error("A carried placement names no asset its store holds");
|
|
const auto& at = carried_[offset].at;
|
|
location = at.location;
|
|
offset = at.offset;
|
|
return true;
|
|
}
|
|
const auto found = archive_ ? held_.find(archive_path(location.installChunk, *archive_).wstring()) : held_.end();
|
|
if (found == held_.end()) throw std::logic_error("A placement waits in a store that holds nothing for it");
|
|
const auto moved = found->second.at + offset;
|
|
if (moved > std::numeric_limits<std::uint32_t>::max())
|
|
throw std::runtime_error("The merged patch's own archive " + path_utf8(fs::path(found->first)) +
|
|
" is full (4 GB); restart Skate so it is built again");
|
|
location.archive = *archive_;
|
|
offset = static_cast<std::uint32_t>(moved);
|
|
return true;
|
|
}
|
|
|
|
void CasStore::shift(fb::CasIdentifier& location, std::uint32_t& offset,
|
|
const ArchivePlacement* placement) const {
|
|
if (!location.patch || !placement) return;
|
|
const auto found = placement->at.find({directory(location.installChunk), location.archive});
|
|
if (found == placement->at.end()) return;
|
|
const auto moved = static_cast<std::uint64_t>(offset) + found->second.offset;
|
|
if (moved > std::numeric_limits<std::uint32_t>::max())
|
|
throw std::runtime_error("Combined mod archives exceed 4 GB, which cas offsets cannot address");
|
|
location.archive = found->second.archive;
|
|
offset = static_cast<std::uint32_t>(moved);
|
|
}
|
|
|
|
} // namespace dingosdk::mods::detail
|