Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191// Links from a pack's content folders into a game directory. The records live in the pack's own// `.packwand/links.json`, and nothing that is not recorded is ever removed. A record is written// before its link exists, so a crash leaves at worst a record with nothing behind it, never a link// nobody knows about.//// A copy link is a real folder kept in step by `sync`. Only files its manifest lists are deleted,// so what the game writes into the copy survives every pass and the unlink. Ported from Packwand's// Zig engine (ledger D-ZIG-LINK-1, D-ZIG-LINK-2). AI-authored (Claude).#pragma once
#include "failure.hpp"#include "fs.hpp"#include "launchers.hpp"#include "mirror.hpp"#include "records.hpp"
#include "packutils.h"
#include <cstdint>#include <optional>#include <string>#include <vector>
namespace packutils::links {
inline constexpr std::string_view directory_name = ".packwand";inline constexpr std::string_view file_name = "links.json";
/// What is in an instance's slot for one folder before linking.enum class Slot : std::uint8_t { free, /// A real directory with nothing in it: replacing it cannot lose anything. empty_directory, /// A link to this folder, or the recorded copy of it. ours, /// Something else; only a forced link moves it, to the backup folder. taken,};
[[nodiscard]] std::string_view name_of(Slot slot);[[nodiscard]] std::optional<Slot> slot_named(std::string_view name);
struct Check { std::string folder; Slot slot = Slot::free; /// The instance's path for the folder. std::string path;};
struct Placed { std::string folder; records::Mode mode = records::Mode::symlink; /// Already linked before this run. bool kept = false; std::optional<std::string> backup; /// Copy links only: source paths the copy could not take. std::vector<std::string> blocked;};
struct Removed { records::Link link; enum class Outcome : std::uint8_t { removed, /// Nothing was there any more. missing, /// A copy that still holds files the game wrote; its record is dropped all the same. left_in_place, } outcome = Outcome::removed; /// The backup a forced link made was moved back. bool restored = false;};
struct SyncReport { std::size_t copied = 0; std::size_t deleted = 0; std::vector<std::string> blocked; std::vector<std::string> failed; /// Copies the instance changed, left as they are. std::vector<std::string> kept; /// Changed in the pack and the instance both; left as they are. std::vector<std::string> conflicts; /// Version 1 entries whose copy differs from the pack's file; left as they are. std::vector<std::string> unknown;};
/// A game directory, and the launcher instance it was named through, if any.struct Resolved { std::string game; std::optional<launchers::Instance> launcher;};
/// What a harvest looks at (packutils SPEC.md section 6).struct HarvestRequest { /// The game directory, as `resolve_instance` gave it. std::string instance; /// The pack's linked folders: never offered as folders to take. std::vector<std::string> pack_folders; /// Names never offered or taken: the game's own output and the pack's state. std::vector<std::string> never; /// Folders the pack lacks, to take whole. std::vector<std::string> folders; /// Top-level files to take. std::vector<std::string> files; /// The pack's mods folder: a jar the instance added there is identified, not copied. std::string mods = "mods";};
struct HarvestFinding { /// `edited`, `conflict`, `unknown`, `added`, `deleted` (copies), `jar`, or `file` (named). std::string kind; /// Relative to the game directory, with `/` separators. std::string path; /// The instance file's SHA-512 (hex) as the scan read it; empty for `deleted`. std::string sha512;};
struct Harvest { std::vector<HarvestFinding> findings; /// Folders the instance has and the pack does not, which `folders` may name. std::vector<std::string> candidates;};
/// How a request asks the engine to work.struct Options { /// A test seam for Wine, which has no byte-range locks. Every real run locks. bool lock_files = true; std::uint32_t lock_timeout_ms = 10'000; /// A test seam: treat directory links as unsupported, as a file system without them would. bool links_unavailable = false; /// The runtime already holds the pack's links lock (`packutils_lock`): take it again neither /// here nor anywhere. bool lock_held = false;};
/// One pack's links, as one request describes them.class PackLinks { public: PackLinks(const pu_host &host, std::string pack_root, Options options) : host_(&host), root_(std::move(pack_root)), options_(options) {}
[[nodiscard]] Result<std::vector<records::Link>> load() const; /// The game directory `instance` names, refused unless it is a directory apart from the pack. /// A launcher's instance folder names the game directory inside it, and says what it runs. [[nodiscard]] Result<Resolved> resolve_instance(const std::string &instance) const; [[nodiscard]] Result<std::vector<Check>> check(const std::string &instance, const std::vector<std::string> &folders) const; /// Links every checked folder. A taken slot is refused unless `force` says to move it to the /// backup folder first. With `copy` no link is tried. A link to this pack's folder that no /// record names is adopted, since only this pack makes one; that is how a crash between placing /// a link and recording it heals. `now` (Unix seconds) names backups. [[nodiscard]] Result<std::vector<Placed>> link(const std::string &instance, const std::vector<Check> &checks, bool force, bool copy, std::int64_t now) const; /// Removes every recorded link into `instance`, or into every instance, and moves back what a /// forced link set aside. A link that is no longer a link, or points elsewhere, is refused and /// kept on record; the links removed before it stay removed. [[nodiscard]] Result<std::vector<Removed>> unlink(const std::optional<std::string> &instance) const; /// One pass of a copy link: the source's changes since the last pass reach the copy. [[nodiscard]] Result<SyncReport> sync(const records::Link &entry) const; /// What the instance changed that the pack does not have, read under the links lock; nothing /// is written anywhere. [[nodiscard]] Result<Harvest> harvest(const HarvestRequest &request) const; /// The links lock, waiting up to the timeout; nothing when the lock is already held or locks /// are off. [[nodiscard]] Result<std::optional<fs::FileLock>> lock() const;
private: const pu_host *host_; std::string root_; Options options_;
[[nodiscard]] bool canceled() const; [[nodiscard]] Result<std::string> state_directory() const; [[nodiscard]] Result<void> save(const std::vector<records::Link> &list) const; [[nodiscard]] Result<SyncReport> sync_locked(const records::Link &entry) const; [[nodiscard]] Result<bool> remove_copy(const records::Link &entry, const std::string &path) const; [[nodiscard]] std::string manifest_path(const records::Link &entry) const; [[nodiscard]] static Result<mirror::Manifest> load_manifest(const std::string &where); /// A file's SHA-512 (hex) through the host, or nothing when it cannot be read. [[nodiscard]] std::optional<std::string> sha512_hex(const std::string &path) const; [[nodiscard]] mirror::Hasher hasher(std::string source_root, std::string target_root) const; [[nodiscard]] Result<void> drop_manifest(const records::Link &entry) const;};
/// `<folder>-<UTC yyyyMMddTHHmmssZ>` for Unix seconds `now`.[[nodiscard]] std::string backup_stem(std::string_view folder, std::int64_t now);
} // namespace packutils::links