Initial commit.

This commit is contained in:
Signal 2026-02-22 17:45:44 -05:00
commit 82b4f23c06
56 changed files with 3485 additions and 0 deletions

6
Shared/CMakeLists.txt Normal file
View file

@ -0,0 +1,6 @@
file(GLOB_RECURSE MY_SOURCES CONFIGURE_DEPENDS
"*.cpp"
"*.h"
)
add_library(Shared ${MY_SOURCES})

91
Shared/Events.h Normal file
View file

@ -0,0 +1,91 @@
#pragma once
#include <unordered_map>
#include <vector>
#include <functional>
#include <typeindex>
#include <any>
#include <algorithm>
class EventTarget {
public:
template<typename T>
using Callback = std::function<void(const T &)>;
// Subscribe to an event type.
template<typename T>
uint64_t listen(Callback<T> callback) {
auto type = std::type_index(typeid(T));
auto & vec = callbacks[type];
uint64_t id = nextEventId++;
vec.emplace_back([cb = std::move(callback)](const std::any & e) {
cb(std::any_cast<const T &>(e));
});
return id;
}
// Unsubscribe a specific listener.
template<typename T>
void unlisten(uint64_t id) {
_unlisten(std::type_index(typeid(T)), id);
}
// Synchronously dispatch an event.
template<typename T>
void dispatch(const T & event) {
auto it = callbacks.find(std::type_index(typeid(T)));
if (it == callbacks.end()) return;
// Copy the list in case a callback unsubscribes during dispatch.
auto copy = it->second;
for (const auto & cb : copy) {
cb(event);
}
}
// Should be called on a main loop to dispatch queued events.
void processQueue() {
for (auto & [anyEvent, copy] : queued) {
auto it = callbacks.find(anyEvent.type());
if (it != callbacks.end()) {
for (const auto & cb : copy) {
cb(anyEvent);
}
}
}
queued.clear();
}
// Queue an event to be dispatched on the next call to `processQueue`.
template<typename T>
void queue(const T & event) {
auto type = std::type_index(typeid(T));
auto it = callbacks.find(type);
if (it == callbacks.end()) return;
queued.emplace_back(std::any(event), it->second);
}
private:
using AnyCallback = std::function<void(const std::any &)>;
using CallbackWithId = std::pair<AnyCallback, uint64_t>;
std::unordered_map<std::type_index, std::vector<AnyCallback>> callbacks {};
std::vector<std::pair<std::any, std::vector<AnyCallback>>> queued {};
uint64_t nextEventId = 0;
void _unlisten(std::type_index type, uint64_t id) {
// auto it = callbacks.find(type);
// if (it == callbacks.end()) return;
//
// auto & vec = it->second;
// auto erase_it = std::find_if(vec.begin(), vec.end(),
// [id](const auto& p) { return p.second == id; });
//
// if (erase_it != vec.end()) {
// *erase_it = std::move(vec.back());
// vec.pop_back();
// }
}
};

View file

@ -0,0 +1,148 @@
#include <string>
#include <thread>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <gnutls/crypto.h>
#include <cista.h>
#include "Connection.h"
namespace Artifact {
Connection::Connection() {
ngtcp2_settings_default(&settings);
ngtcp2_transport_params_default(&transportParams);
transportParams.initial_max_streams_bidi = 1;
transportParams.initial_max_data = 65535;
transportParams.initial_max_stream_data_bidi_local = 65535;
callbacks.encrypt = ngtcp2_crypto_encrypt_cb;
callbacks.decrypt = ngtcp2_crypto_decrypt_cb;
callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb;
callbacks.update_key = ngtcp2_crypto_update_key_cb;
callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb;
callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb;
callbacks.rand = [](auto dest, auto len, auto ctx) {
gnutls_rnd(GNUTLS_RND_RANDOM, dest, len);
};
callbacks.get_new_connection_id = [](auto conn, auto cid, auto token, auto cidlen, auto userdata) {
gnutls_rnd(GNUTLS_RND_RANDOM, cid, cidlen);
gnutls_rnd(GNUTLS_RND_RANDOM, token, NGTCP2_STATELESS_RESET_TOKENLEN);
return 0;
};
callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb;
gnutls_rnd(GNUTLS_RND_RANDOM, dcid.data, dcid.datalen);
gnutls_rnd(GNUTLS_RND_RANDOM, scid.data, scid.datalen);
}
void readPacket(ngtcp2_pkt_info * info, void * packet) {
}
ClientConnection::ClientConnection(std::string host, std::string port) {
struct addrinfo hints;
struct addrinfo * local = nullptr;
struct addrinfo * remote = nullptr;
getaddrinfo("::", "", &hints, &local);
getaddrinfo(host.c_str(), port.c_str(), &hints, &remote);
ngtcp2_path path = {
.local = {
.addr = local->ai_addr,
.addrlen = local->ai_addrlen
},
.remote = {
.addr = remote->ai_addr,
.addrlen = remote->ai_addrlen
}
};
if (gnutls_init(&session, GNUTLS_CLIENT) != GNUTLS_E_SUCCESS) {
throw std::runtime_error("Failed to initialize gnutls.");
}
gnutls_set_default_priority(session);
ngtcp2_conn_client_new(&conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, &callbacks, &settings, &transportParams, nullptr, this);
ngtcp2_conn_set_tls_native_handle(conn, session);
}
ServerConnection::ServerConnection(ngtcp2_path path) : path(path) {
if (gnutls_init(&session, GNUTLS_CLIENT) != GNUTLS_E_SUCCESS) {
throw std::runtime_error("Failed to initialize gnutls.");
}
gnutls_set_default_priority(session);
ngtcp2_conn_server_new(&conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, &callbacks, &settings, &transportParams, nullptr, this);
ngtcp2_conn_set_tls_native_handle(conn, session);
}
ServerListener::ServerListener(std::string port) {
struct addrinfo hints;
struct addrinfo * local = nullptr;
getaddrinfo("::", "", &hints, &local);
socketDescriptor = socket(local->ai_family, local->ai_socktype, local->ai_protocol);
int on = 1;
setsockopt(socketDescriptor, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char *>(&on), sizeof(int));
if (local->ai_family == AF_INET6) {
int off = 0;
setsockopt(socketDescriptor, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<char *>(&off), sizeof(int));
}
if (bind(socketDescriptor, local->ai_addr, local->ai_addrlen)) {
// Save our actual local address for later use.
getsockname(socketDescriptor, reinterpret_cast<sockaddr *>(&localAddr.addr), &localAddr.addrlen);
}
}
void ServerListener::onReadable(struct ev_loop *, ev_io * watcher, int) {
auto self = static_cast<ServerListener *>(watcher->data);
uint8_t buf[2048];
ngtcp2_addr remote;
auto len = recvfrom(self->socketDescriptor, buf, sizeof(buf), 0, remote.addr, &remote.addrlen);
ngtcp2_pkt_info pi{};
ngtcp2_pkt_hd hd{};
ngtcp2_pkt_decode_hd_long(&hd, buf, len);
auto it = self->connections.find(hd.dcid);
if (it != self->connections.end()) {
ServerConnection * conn = it->second.get();
ngtcp2_conn_read_pkt(conn->conn, &conn->path, &pi, buf, len, std::chrono::steady_clock::now().time_since_epoch().count());
conn->readPacket(&pi, buf);
return;
}
if (hd.flags & NGTCP2_PKT_FLAG_LONG_FORM && hd.type == NGTCP2_PKT_INITIAL) {
ngtcp2_pkt_hd accept_hd{};
if (ngtcp2_accept(&accept_hd, buf, static_cast<size_t>(len)) != 0) {
return;
}
auto new_conn = std::make_unique<ServerConnection>(ngtcp2_path {
.local = self->localAddr,
.remote = remote
});
self->connections[accept_hd.dcid] = std::move(new_conn);
}
}
void ServerListener::run() {
std::thread([this]() {
loop = ev_loop_new(EVFLAG_AUTO);
ev_io_init(&watcher, onReadable, socketDescriptor, EV_READ);
watcher.data = this;
ev_io_start(loop, &watcher);
ev_run(loop, 0);
}).detach();
}
}

View file

@ -0,0 +1,95 @@
#pragma once
#include <string>
#include <unordered_map>
#include <ngtcp2/ngtcp2.h>
#include <gnutls/gnutls.h>
#include <ev.h>
// Platform-specific socket includes and helpers
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
using socket_t = SOCKET;
#define CLOSE_SOCKET closesocket
#define SOCKLEN_T int
#define IS_INVALID_SOCKET(s) ((s) == INVALID_SOCKET)
#define GET_LAST_ERROR() WSAGetLastError()
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
using socket_t = int;
#define CLOSE_SOCKET close
#define SOCKLEN_T socklen_t
#define IS_INVALID_SOCKET(s) ((s) < 0)
#define GET_LAST_ERROR() errno
#endif
namespace Artifact {
namespace {
struct Ngtcp2CidEqual {
bool operator()(const ngtcp2_cid& a, const ngtcp2_cid& b) const noexcept {
return a.datalen == b.datalen &&
std::memcmp(a.data, b.data, a.datalen) == 0;
}
};
struct Ngtcp2CidHash {
size_t operator()(const ngtcp2_cid& cid) const noexcept {
size_t h = cid.datalen;
for (uint8_t i = 0; i < cid.datalen; ++i) {
h = (h * 31) ^ cid.data[i]; // or better hash as above
}
return h;
}
};
}
class Connection {
public:
ngtcp2_settings settings;
ngtcp2_transport_params transportParams;
ngtcp2_callbacks callbacks;
ngtcp2_cid dcid, scid;
gnutls_session_t session;
ngtcp2_conn * conn;
Connection();
void readPacket(ngtcp2_pkt_info * info, void * packet);
};
class ClientConnection: public Connection {
public:
ClientConnection(std::string host, std::string port);
};
class ServerConnection: public Connection {
public:
ngtcp2_path path;
ServerConnection(ngtcp2_path path);
};
class ServerListener {
socket_t socketDescriptor;
std::unordered_map<ngtcp2_cid, std::unique_ptr<ServerConnection>, Ngtcp2CidHash, Ngtcp2CidEqual> connections;
ngtcp2_addr localAddr;
struct ev_loop * loop;
ev_io watcher;
public:
ServerListener(std::string port);
static void onReadable(struct ev_loop *, ev_io * watcher, int);
void run();
};
}

View file

@ -0,0 +1,31 @@
#include "Network.h"
namespace Artifact {
void NetworkServer::host(NetworkClient * local) {
if (active) {
unhost();
}
active = true;
localClient = local;
listener = local->listen<Events::NetworkMessage>([](auto ev) {
});
}
void NetworkServer::unhost() {
active = false;
if (localClient) {
localClient->unlisten<Events::NetworkMessage>(listener);
localClient = nullptr;
} else if (server) {
server = nullptr;
}
}
void NetworkClient::connect(NetworkServer * local) {
localServer = local;
}
}

45
Shared/Network/Network.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include "Shared.h"
#include "Events.h"
#include "Connection.h"
namespace Artifact {
namespace Events {
struct NetworkMessage {
void * data;
};
}
class NetworkClient;
class NetworkServer: public BaseSubsystem, public EventTarget {
NetworkClient * localClient = nullptr;
std::unique_ptr<ServerListener> server = nullptr;
bool active = false;
uint64_t listener;
public:
void host(NetworkClient * client);
void host(std::string port);
void unhost();
};
class NetworkClient: public BaseSubsystem, public EventTarget {
NetworkServer * localServer = nullptr;
std::unique_ptr<Connection> server = nullptr;
bool active = false;
uint64_t listener;
public:
void connect(NetworkServer * local);
void connect(std::string host, std::string port);
void disconnect();
};
}

60
Shared/Paths.cpp Normal file
View file

@ -0,0 +1,60 @@
#include "Paths.h"
namespace Artifact {
Path getDataPath() {
Path base;
// ────────────────────────────────────────────────
// 1. Windows — AppData/Roaming (most common choice)
// ────────────────────────────────────────────────
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
if (!appdata || !fs::exists(appdata)) {
throw std::runtime_error("Cannot find APPDATA environment variable");
}
base = Path(appdata) / organization / GAME;
#elif defined(__APPLE__)
// ────────────────────────────────────────────────
// 2. macOS — Application Support
// ────────────────────────────────────────────────
const char* home = std::getenv("HOME");
if (!home) throw std::runtime_error("Cannot find HOME");
base = Path(home) / "Library" / "Application Support" / GAME;
#else
// ────────────────────────────────────────────────
// 3. Linux / BSD / Steam Deck — XDG_DATA_HOME
// ────────────────────────────────────────────────
const char* xdg_data = std::getenv("XDG_DATA_HOME");
if (xdg_data && *xdg_data) {
base = fs::path(xdg_data) / game;
} else {
const char* home = std::getenv("HOME");
if (!home) throw std::runtime_error("Cannot find HOME");
base = Path(home) / ".local" / "share" / GAME;
}
#endif
// Create folder structure if it doesn't exist
std::error_code ec;
std::filesystem::create_directories(base, ec);
if (ec) {
throw std::runtime_error("Failed to create directory: " +
base.string() + "" + ec.message());
}
return base;
}
Path getClientConfigPath() {
return getDataPath() / "client.conf";
}
Path getServerConfigPath() {
return getDataPath() / "server.conf";
}
}

24
Shared/Paths.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <filesystem>
#include <iostream>
#ifndef APP_NAME
# define APP_NAME ArtifactEngine
#endif
#define TOSTRING(x) #x
#define GAME TOSTRING(APP_NAME)
namespace Artifact {
using Path = std::filesystem::path;
Path getDataPath();
Path getClientConfigPath();
Path getServerConfigPath();
}

58
Shared/Settings.cpp Normal file
View file

@ -0,0 +1,58 @@
#include "Settings.h"
namespace Artifact {
Settings::Settings() : Settings(getDataPath() / "settings.conf") {}
Settings::Settings(Path path) : path(path) {
reload();
}
std::string trim(const std::string& str) {
size_t first = str.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return "";
size_t last = str.find_last_not_of(" \t\r\n");
return str.substr(first, last - first + 1);
}
void Settings::reload() {
clear();
std::ifstream file(path);
if (!file.is_open()) {
// TODO: Log a warning
return;
}
std::string line;
while (std::getline(file, line)) {
line = trim(line);
if (line.empty() || line[0] == '#' || line[0] == ';') {
continue;
}
size_t eq_pos = line.find('=');
if (eq_pos == std::string::npos) {
continue;
}
std::string key = trim(line.substr(0, eq_pos));
std::string value = trim(line.substr(eq_pos + 1));
if (!key.empty()) {
data[key] = value;
}
}
}
void Settings::save() {
std::ofstream file(path);
if (!file.is_open()) {
return;
}
for (const auto & [key, value] : data) {
file << key << " = " << value << '\n';
}
}
}

85
Shared/Settings.h Normal file
View file

@ -0,0 +1,85 @@
#pragma once
#include <string>
#include <fstream>
#include <sstream>
#include <optional>
#include "Paths.h"
#include "Events.h"
namespace Artifact {
namespace Events {
struct SettingChanged {
std::string key;
std::optional<std::string> value;
};
}
class Settings: public EventTarget {
const Path path;
std::unordered_map<std::string, std::string> data;
public:
Settings();
Settings(Path path);
void reload();
void save();
template<typename T>
T get(std::string & key, const T & fallback = {}) {
auto it = data.find(key);
if (it == data.end()) {
return fallback;
}
const std::string & str = it->second;
if constexpr (std::is_same_v<T, std::string>) {
return str;
}
if constexpr (std::is_same_v<T, bool>) {
std::string lower = str;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") {
return true;
}
if (lower == "false" || lower == "0" || lower == "no" || lower == "off") {
return false;
}
return fallback;
}
std::istringstream iss(str);
T value;
if (iss >> value) {
return value;
}
return fallback;
}
template<typename T>
void set(const std::string & key, const T & value) {
std::ostringstream oss;
oss << std::boolalpha << value;
data[key] = oss.str();
dispatch(Events::SettingChanged { .key = key, .value = value });
}
void reset(const std::string & key) {
data.erase(key);
dispatch(Events::SettingChanged { .key = key, .value = std::nullopt });
}
void clear() {
data.clear();
}
};
}

11
Shared/Shared.cpp Normal file
View file

@ -0,0 +1,11 @@
#include <iostream>
#include "Shared.h"
namespace Artifact {
void log(std::string msg) {
std::cout << msg << std::endl;
}
}

147
Shared/Shared.h Normal file
View file

@ -0,0 +1,147 @@
#pragma once
#include <vector>
#include <glm/glm.hpp>
namespace std {
template<>
struct hash<glm::ivec3> {
std::size_t operator()(const glm::ivec3& v) const noexcept {
// Very common and reasonably good quality combination for floats
// (tries to mix bits while being fast)
std::size_t seed = 0;
// You can use any of these styles — pick one:
// ────────────────────────────────────────────────────────────────
// Style 1: Boost hash_combine pattern (very popular)
auto hash_float = [](float f) -> std::size_t {
return std::hash<float>{}(f);
};
seed ^= hash_float(v.x) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
seed ^= hash_float(v.y) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
seed ^= hash_float(v.z) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
// ────────────────────────────────────────────────────────────────
// Style 2: Simpler — fold with multiplication (often enough)
// seed = std::hash<float>{}(v.x);
// seed = seed * 31 + std::hash<float>{}(v.y);
// seed = seed * 31 + std::hash<float>{}(v.z);
// ────────────────────────────────────────────────────────────────
// Style 3: Treat as 12-byte blob → good quality, but slower on some platforms
// std::size_t h = 0;
// std::memcpy(&h, &v, sizeof(float)); // x
// h ^= std::hash<std::uint32_t>{}(h);
// std::memcpy(&h, reinterpret_cast<const char*>(&v) + 4, sizeof(float));
// h ^= std::hash<std::uint32_t>{}(h) * 0x85ebca6b;
// std::memcpy(&h, reinterpret_cast<const char*>(&v) + 8, sizeof(float));
// return std::hash<std::uint32_t>{}(h);
return seed;
}
};
} // namespace std
namespace Artifact {
template<typename Subsystem>
class Engine {
public:
std::vector<std::unique_ptr<Subsystem>> subsystems;
template <typename T, typename... Args>
T * addSubsystem(Args &&... args) {
auto system = std::make_unique<T>(std::forward<Args>(args)...);
auto ptr = system.get();
subsystems.push_back(std::move(system));
return ptr;
}
template <typename T>
T * subsystem() const {
for (const auto & system : subsystems) {
if (T * ptr = dynamic_cast<T *>(system.get())) {
return ptr;
}
}
return nullptr;
}
};
class BaseSubsystem {
public:
virtual void init() {}
virtual void reload() {}
virtual void deinit() {}
};
template<typename T> requires std::integral<T> || std::floating_point<T>
class Composed {
enum ModifierType {
Add, Multiply
};
using Modifier = std::pair<ModifierType, T>;
uint64_t nextID = 0;
std::vector<Modifier> modifiers;
T _value;
public:
Composed() : Composed(T{0}) {}
Composed(T base) {
add(Add, base);
}
T value() {
return _value;
}
void recompute() {
T sum;
T fac;
for (auto [type, value] : modifiers) {
if (type == Add) {
sum += value;
} else if (type == Multiply) {
fac *= value;
}
}
_value = sum * fac;
}
uint64_t add(ModifierType type, T value) {
modifiers.emplace_back(type, value);
recompute();
return nextID++;
}
bool remove(uint64_t id) {
auto it = std::find_if(modifiers.begin(), modifiers.end(),
[id](const Modifier & m) { return m.id == id; });
if (it == modifiers.end()) return false;
modifiers.erase(it);
recompute();
return true;
}
bool update(uint64_t id, T newValue) {
auto it = std::find_if(modifiers.begin(), modifiers.end(),
[id](const Modifier& m) { return m.id == id; });
if (it == modifiers.end()) return false;
it->value = newValue;
recompute();
return true;
}
};
void log(std::string msg);
}

9
Shared/World/Chunk.h Normal file
View file

@ -0,0 +1,9 @@
#include <vector>
#include <glm/glm/glm.hpp>
struct Chunk {
glm::ivec3 pos;
std::vector<uint16_t> data;
bool dirty = false;
};