#ifndef HYPERSWARM_OVERLAY_NODE_H
#define HYPERSWARM_OVERLAY_NODE_H
// The Godot face of liboverlay: fetch content-addressed assets from peers, and
// serve the ones this instance holds.
//
// WHICH THREAD OWNS WHAT — see DESIGN-transfer-manager.md §2.1, which this file
// got wrong once and now follows.
//
// UDX thread owns the Protocol OUTRIGHT. Protomux dispatches inbound frames
// here, and udx/protomux handles belong to this loop — so both
// halves of the protocol must live here or they race. It is also
// what keeps step 6 honest: send() returns writability
// synchronously, and that answer only means anything on the
// thread that owns the socket.
// main Godot's. Issues commands (marshalled via run_on_loop) and
// drains an event queue. Signals are emitted ONLY from poll().
//
// There is no worker thread. An earlier version had one owning the Protocol,
// which was unsafe on both counts above. What that costs is stated rather than
// hidden: the two genuinely unbounded operations — building a chunk map for a
// file being served for the first time, and re-verifying a partial whose journal
// is missing — run on the UDX thread. Both are kept off the hot path instead of
// off the thread: serve() precomputes and caches the chunk map when the GAME
// registers an asset, not when a peer asks; and journaled resume makes
// re-verification O(1) in the normal case. Per-chunk work (one blake2b over a
// chunk, one positioned write) is bounded and stays inline.
//
// Moving those two onto a worker is the remaining refinement, and it needs the
// async chunk hand-off §2.1 describes. It is not needed to move bytes.
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/variant/string.hpp>
#include "hyperswarm_peer.h"
#include "overlay/file_index.h"
#include "overlay/protocol.h"
#include "overlay_transport.h"
#include "udx_transport.h"
namespace godot {
class HyperOverlay : public Node {
GDCLASS(HyperOverlay, Node)
public:
HyperOverlay();
~HyperOverlay();
// Open the local index and start the worker. `storage_dir` holds the index
// log, the journals and the content spool; it must survive restarts, because
// that is what makes a resumed download cheap.
bool open(const String &storage_dir);
void close();
bool is_open() const { return open_; }
// Attach to a HyperswarmMultiplayerPeer's connections. From here on, every
// peer that completes its Noise handshake gets an overlay channel and joins
// the fetch pool; every peer that drops hands its outstanding chunks back.
bool attach(Object *hyperswarm_peer); // Object, not Node: the peer is RefCounted
void detach();
int peer_count() const;
// The content hash of a file, computed the way the overlay computes it —
// blake2b over the hypercore leaf construction, not a plain digest. A game
// needs this to name what it serves and to ask for what it wants, and getting
// it from anywhere else yields a hash the overlay will reject.
String content_hash_of(const String &file_path) const;
// Declare that this instance holds `content_hash` at `file_path`, and will
// serve it to peers that ask. The file is NOT read here — the overlay reads
// chunks at their offsets when they are requested, so registering a 4 GB asset
// costs nothing.
void serve(const String &content_hash, const String &file_path);
// Ask connected peers for content, to be assembled at `dest_path`. Returns
// immediately; watch the signals. A partial left by an earlier attempt is
// resumed rather than restarted.
void fetch(const String &content_hash, const String &dest_path);
// Stop fetching. `discard` unlinks the partial; otherwise it is kept so a
// later fetch of the same hash resumes it.
void cancel(const String &content_hash, bool discard);
// Drain completions onto the main thread and advance liboverlay's clock.
// Called automatically from _process; exposed for headless use and tests,
// where there is no scene tree ticking.
void poll();
void _process(double delta) override;
// Diagnostics, because "it is downloading" is not an answer when it is not.
int active_fetches() const;
String describe() const;
protected:
static void _bind_methods();
private:
// Work handed to the worker, and results handed back. Deliberately plain
// values: nothing that owns a file handle or a Godot object crosses a thread.
struct Event {
enum Kind { FETCHED, FAILED, PROGRESS } kind;
std::string hash, detail;
uint64_t received = 0, total = 0;
};
void push_event(const Event &e);
// Run on the UDX thread, or inline when nothing is attached yet.
void on_loop(std::function<void()> fn);
// Everything below runs ON THE UDX THREAD.
void udx_add_peer(int32_t id, Protomux *mux);
void udx_remove_peer(int32_t id);
std::unique_ptr<hyperoverlay::index::FileIndex> index_;
std::unique_ptr<hyperoverlay::Protocol> proto_;
bool open_ = false;
std::string storage_dir_;
// The peer is RefCounted and the transport is a MEMBER of it, so borrowing the
// pointer without holding the object is borrowing memory the game may free at
// any moment. It does: a room switch drops its reference before handing us the
// replacement, and detach() then tore down a freed transport — corrupting the
// heap, which the NEXT peer's uv_loop_init reported as its own abort. Holding
// the peer here means the transport we point at cannot die before we let go of
// it. See integration/layer14_room_switch.gd.
Ref<HyperswarmMultiplayerPeer> peer_;
UdxTransport *transport_ = nullptr;
// One overlay channel per connection, created and destroyed on the UDX thread.
std::map<int32_t, hyperswarm::OverlayTransport *> peers_;
mutable std::mutex evt_mutex_;
std::deque<Event> events_;
// Bounded: a fetch that emits progress faster than the game polls must drop
// updates, not accumulate them. Terminal events (FETCHED/FAILED) are never
// dropped — losing one strands a caller waiting for a signal that never comes.
static constexpr size_t MAX_EVENTS = 4096;
size_t dropped_progress_ = 0;
uint64_t started_ms_ = 0;
};
} // namespace godot
#endif // HYPERSWARM_OVERLAY_NODE_H