From a5b377745116de2b269cdfc5b90f08f46d0432d2 Mon Sep 17 00:00:00 2001 From: hovertank3d Date: Thu, 13 Nov 2025 23:02:56 +0100 Subject: initial commit --- include/net/client.hpp | 51 +++++ include/net/conn.hpp | 188 ++++++++++++++++++ include/net/packet.hpp | 98 ++++++++++ include/net/server.hpp | 65 +++++++ include/net/types.hpp | 503 ++++++++++++++++++++++++++++++++++++++++++++++++ include/util/bitset.hpp | 48 +++++ include/util/endian.hpp | 21 ++ include/version.hpp | 3 + include/world/world.hpp | 12 ++ 9 files changed, 989 insertions(+) create mode 100644 include/net/client.hpp create mode 100644 include/net/conn.hpp create mode 100644 include/net/packet.hpp create mode 100644 include/net/server.hpp create mode 100644 include/net/types.hpp create mode 100644 include/util/bitset.hpp create mode 100644 include/util/endian.hpp create mode 100644 include/version.hpp create mode 100644 include/world/world.hpp (limited to 'include') diff --git a/include/net/client.hpp b/include/net/client.hpp new file mode 100644 index 0000000..8f81638 --- /dev/null +++ b/include/net/client.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "net/conn.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace net { + +class client { + struct sockaddr_in server_address {}; + +public: + client(std::string hostname, int port) + { + server_address.sin_family = AF_INET; + inet_pton(AF_INET, hostname.c_str(), &server_address.sin_addr); + server_address.sin_port = htons(port); + } + + conn connect() + { + int sock_fd; + + sock_fd = socket(AF_INET, SOCK_STREAM, 0); + if (sock_fd < 0) + throw std::runtime_error(strerror(errno)); + + auto err = ::connect(sock_fd, (struct sockaddr*)&server_address, + sizeof(server_address)); + if (err < 0) + throw std::runtime_error(strerror(errno)); + + return conn { sock_fd, server_address, sizeof(server_address) }; + } +}; +}; diff --git a/include/net/conn.hpp b/include/net/conn.hpp new file mode 100644 index 0000000..b69dac6 --- /dev/null +++ b/include/net/conn.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "net/packet.hpp" + +namespace net { +class conn { + int sock_fd; + + sockaddr_in conn_addr; + socklen_t conn_addr_size; + + using handler_list = std::vector)>>; + std::array handlers; + std::map netmodule_handlers; + +private: + packet::packet_header read_header() + { + uint16_t packet_size; + uint8_t id; + + recv(packet_size); + recv(id); + + return { id, packet_size - 3}; + } + +public: + conn(int sock_fd, sockaddr_in conn_addr, socklen_t conn_addr_size) + : sock_fd(sock_fd) + , conn_addr(conn_addr) + , conn_addr_size(conn_addr_size) + { + } + + ~conn() + { + close(sock_fd); + } + + ssize_t send(std::span data) + { + auto bytes = ::send(sock_fd, data.data(), data.size_bytes(), 0); + if (bytes < 0) { + throw std::runtime_error(strerror(errno)); + } + return bytes; + } + + template + ssize_t send(T t) + { + t = to_little(t); + auto bytes = ::send(sock_fd, &t, sizeof(T), 0); + if (bytes < 0) { + throw std::runtime_error(strerror(errno)); + } + return bytes; + } + + ssize_t recv(std::span data) + { + if (data.size_bytes() == 0) { + return 0; + } + + auto bytes = ::recv(sock_fd, data.data(), data.size_bytes(), 0); + if (bytes <= 0) { + throw std::runtime_error(strerror(errno)); + } + return bytes; + } + + template + ssize_t recv(T& t) + { + auto bytes = ::recv(sock_fd, &t, sizeof(T), 0); + if (bytes <= 0) { + throw std::runtime_error(strerror(errno)); + } + t = to_little(t); + if (bytes != sizeof(T)) { + throw std::runtime_error(strerror(errno)); + } + return bytes; + } + + template + void expect_packet(T& t) + { + auto [id, len] = read_header(); + assert(id == T::packet_id); + + if(len > 0) { + std::vector payload(len); + recv(payload); + decode_packet(payload, t); + } + } + + template + void reg_handler(H h) { + handlers[P::packet_id].push_back([this, h](std::span payload) { + P t; + if (payload.size() > 0) { + packet::decode_packet(payload, t); + } + return h(t); + }); + } + + template + void reg_handler(H h) { + netmodule_handlers[P::module_id].push_back([this, h](std::span payload) { + P t; + if (payload.size() > 0) { + packet::decode_packet(payload, t); + } + return h(t); + }); + } + + template + void send_packet(T p) + { + auto payload = encode_packet(p); + + send(int16_t(payload.size() + 3)); + send(T::packet_id); + if (payload.size() > 0) { + send(payload); + } + } + + bool handle_netmodule(std::span payload) { + uint16_t id; + std::memcpy(&id, payload.data(), sizeof(id)); + id = to_little(id); + + if (netmodule_handlers[id].size() <= 0) { + // currently i won't bother implementing netmodules + // FIXME: return false + return true; + } + + for (auto &h : handlers[id]) { + if (h(payload.subspan(2)) == true) { + return false; + } + } + return true; + } + + bool handle() { + auto [id, len] = read_header(); + if (id == 0) { + return false; + } + + std::vector payload(len); + recv(payload); + + //got netmodule + if (id == 82) { + return handle_netmodule(payload); + } + + if (handlers[id].size() <= 0) { + return false; + } + + for (auto &h : handlers[id]) { + if (h(payload) == true) { + break; + } + } + return true; + } + +}; +} \ No newline at end of file diff --git a/include/net/packet.hpp b/include/net/packet.hpp new file mode 100644 index 0000000..f2094c0 --- /dev/null +++ b/include/net/packet.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "types.hpp" + +namespace net { + +namespace packet { + +using packet_header = std::tuple; + +template +void decode_packet(std::span payload, T& t) +{ + std::size_t offset = 0; + + boost::pfr::for_each_field(t, [&payload, &offset](auto& field, int i) { + if (offset >= payload.size()) { + throw std::runtime_error("Received invalid packet"); + } + offset += decode_field(payload.subspan(offset), field); + }); + + if (offset < payload.size()) { + throw std::runtime_error(std::format("{}: {}", offset, payload.size())); + } +} + +template +std::vector encode_raw_packet(T& t) +{ + std::vector payload; + + boost::pfr::for_each_field(t, [&payload](auto& field) { + encode_field(payload, field); + }); + + return payload; +} + +template +std::vector encode_packet(T& t) +{ + return encode_raw_packet(t); +} + +template +std::vector encode_packet(T& t) +{ + static constexpr auto chunk_size = 1024; + std::array buffer; + std::vector payload; + + int ret, flush; + z_stream strm; + + auto raw_payload = encode_raw_packet(t); + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + ret = deflateInit(&strm, 15); + if (ret != Z_OK) + throw std::runtime_error("deflate init error"); + + strm.avail_in = raw_payload.size(); + strm.next_in = raw_payload.data(); + do { + strm.avail_out = chunk_size; + strm.next_out = reinterpret_cast(buffer.data()); + ret = deflate(&strm, flush); + assert(ret != Z_STREAM_ERROR); + payload.insert(payload.end(), buffer.begin(), buffer.end()-strm.avail_out); + } while (strm.avail_out == 0); + + assert(strm.avail_in == 0); /* all input will be used */ + + deflateEnd(&strm); + return payload; +} +} + +} \ No newline at end of file diff --git a/include/net/server.hpp b/include/net/server.hpp new file mode 100644 index 0000000..14a5d1d --- /dev/null +++ b/include/net/server.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "net/conn.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace net { + +class server { + struct sockaddr_in server_address {}; + int sock_fd; + +public: + + server(std::string hostname, int port) + { + server_address.sin_family = AF_INET; + inet_pton(AF_INET, hostname.c_str(), &server_address.sin_addr); + server_address.sin_port = htons(port); + } + + ~server() { + close(sock_fd); + } + + void bind() { + sock_fd = socket(AF_INET, SOCK_STREAM, 0); + if (sock_fd < 0) + throw std::runtime_error(strerror(errno)); + + int bindStatus = ::bind(sock_fd, (struct sockaddr*)&server_address, sizeof(server_address)); + if (bindStatus < 0) + throw std::runtime_error(strerror(errno)); + } + + void listen(int max_connections) { + ::listen(sock_fd, max_connections); + } + + conn accept() { + sockaddr_in conn_addr {}; + socklen_t conn_addr_size = sizeof(conn_addr); + + int conn_fd = ::accept(sock_fd, (struct sockaddr *)&conn_addr, &conn_addr_size); + if (conn_fd < 0) + throw std::runtime_error(strerror(errno)); + return conn { conn_fd, conn_addr, conn_addr_size }; + } +}; +} \ No newline at end of file diff --git a/include/net/types.hpp b/include/net/types.hpp new file mode 100644 index 0000000..9ebc56c --- /dev/null +++ b/include/net/types.hpp @@ -0,0 +1,503 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util/bitset.hpp" +#include "util/endian.hpp" +#include "version.hpp" + +namespace net { +namespace packet { + +using packet_flag = struct { }; + +template +concept packet = requires(T x) { x.packet_id; }; + +template +concept compressed_packet = requires(T x) { packet && x.compressed; }; + +template +concept netmodule = requires(T x) { x.module_id; }; + +struct nstring : std::string { + enum : uint8_t { + Literal, + Formattable, + LocalizationKey, + } mode; + + std::string s; + + nstring(std::string&& s) + : mode(Literal) + , s(s) + { + } + + nstring(std::string& s) + : mode(Literal) + , s(s) + { + } +}; + +struct rgb { + uint8_t r; + uint8_t g; + uint8_t b; +}; + +struct death_reason { + enum class reason : int { + Player = 0, + NPC, + Projectile, + Other, + ProjectileType, + Item, + ItemPrefix, + Custom, + }; + + bitset<8> reasons; + + int16_t player_index; + int16_t npc_index; + int16_t projectile_index; + uint8_t other_index; + int16_t projectile_type; + int16_t item_type; + uint8_t item_prefix; + std::string custom; +}; + +void encode_field(std::vector& data, const std::string& f) +{ + auto bytes = reinterpret_cast(f.data()); + data.insert(data.end(), uint8_t(f.size())); + data.insert(data.end(), bytes, bytes + f.size()); +} + +template +void encode_field(std::vector& data, T f) +{ + auto fixed = to_little(f); + auto* p = reinterpret_cast(&fixed); + data.insert(data.end(), p, p + sizeof(T)); +} + +template +void encode_field(std::vector& data, T f) +{ + auto x = reinterpret_cast(&f); + data.insert(data.end(), x, x + sizeof(T)); +} + +void encode_field(std::vector& data, nstring& s) +{ + data.insert(data.end(), uint8_t(s.mode)); + encode_field(data, s.s); +} + +void encode_field(std::vector& data, const rgb& f) +{ + encode_field(data, f.r); + encode_field(data, f.g); + encode_field(data, f.b); +} + +template +void encode_field(std::vector& data, std::array& f) +{ + for (auto& e : f) { + encode_field(data, e); + } +} + +template +void encode_field(std::vector& data, std::vector &f) +{ + for (auto& e : f) { + encode_field(data, e); + } +} + +template +void encode_field(std::vector& data, std::span &f) +{ + for (auto& e : f) { + encode_field(data, e); + } +} + +template +void encode_field(std::vector& data, bitset &f) +{ + for (auto& e : f) { + encode_field(data, e); + } +} + +void encode_field(std::vector& data, death_reason &f) +{ + encode_field(data, f.reasons); + if (f.reasons[death_reason::reason::Player]) { + encode_field(data, f.player_index); + } + if (f.reasons[death_reason::reason::NPC]) { + encode_field(data, f.npc_index); + } + if (f.reasons[death_reason::reason::Projectile]) { + encode_field(data, f.projectile_index); + } + if (f.reasons[death_reason::reason::Other]) { + encode_field(data, f.other_index); + } + if (f.reasons[death_reason::reason::ProjectileType]) { + encode_field(data, f.projectile_type); + } + if (f.reasons[death_reason::reason::Item]) { + encode_field(data, f.item_type); + } + if (f.reasons[death_reason::reason::ItemPrefix]) { + encode_field(data, f.item_prefix); + } + if (f.reasons[death_reason::reason::Custom]) { + encode_field(data, f.custom); + } +} + +ssize_t decode_field(std::span data, std::string& f) +{ + assert(data.size() > 0); + uint8_t size = uint8_t(data[0]); // FIXME: varint + + assert(data.size() >= std::size_t(size + 1)); + + f.resize(size); + f.assign(reinterpret_cast(data.data() + 1 /* FIXME: varint*/), size); + return size + 1; +} + +template +ssize_t decode_field(std::span data, T& f) +{ + assert(data.size() >= sizeof(T)); + std::memcpy(&f, data.data(), sizeof(T)); + f = to_little(f); + return sizeof(T); +} + + +ssize_t decode_field(std::span data, std::vector& f) +{ + f.insert(f.begin(), data.begin(), data.end()); + return f.size(); +} + + +template +ssize_t decode_field(std::span data, std::array& f) +{ + assert(data.size() >= sizeof(T) * sz); + + ssize_t offset = 0; + for (auto& e : f) { + offset += decode_field(data.subspan(offset), e); + } + + return offset; +} + +ssize_t decode_field(std::span data, death_reason &f) +{ + int offset = 0; + offset += decode_field(data, f.reasons); + if (f.reasons[0]) { + offset += decode_field(data.subspan(offset), f.player_index); + } + if (f.reasons[1]) { + offset += decode_field(data.subspan(offset), f.npc_index); + } + if (f.reasons[2]) { + offset += decode_field(data.subspan(offset), f.projectile_index); + } + if (f.reasons[3]) { + offset += decode_field(data.subspan(offset), f.other_index); + } + if (f.reasons[4]) { + offset += decode_field(data.subspan(offset), f.projectile_type); + } + if (f.reasons[5]) { + offset += decode_field(data.subspan(offset), f.item_type); + } + if (f.reasons[6]) { + offset += decode_field(data.subspan(offset), f.item_prefix); + } + if (f.reasons[7]) { + offset += decode_field(data.subspan(offset), f.custom); + } + + return offset; +} +ssize_t decode_field(std::span data, rgb& f) +{ + assert(data.size() >= 3); + + f.r = uint8_t(data[0]); + f.g = uint8_t(data[1]); + f.b = uint8_t(data[2]); + return 3; +} + +struct conn_request { + static constexpr uint8_t packet_id = 1; + + std::string ver; +}; + +struct disconnect { + static constexpr uint8_t packet_id = 2; + + nstring reason; +}; + +struct accept { + static constexpr uint8_t packet_id = 3; + + std::uint8_t client_id; + uint8_t _ = 0; +}; + +struct player_info { + static constexpr uint8_t packet_id = 4; + + uint8_t client_id; + uint8_t skin_variant; + uint8_t hair_variant; + std::string name; + uint8_t hair_dye; + uint16_t hide_visuals; + uint8_t hide_misc; + rgb hair_color; + rgb skin_color; + rgb eye_color; + rgb shirt_color; + rgb undershirt_color; + rgb pants_color; + rgb shoes_color; + uint8_t difficulty; + uint8_t torches; + uint8_t permanent_buffs; +}; + +struct player_inventory_slot { + static constexpr uint8_t packet_id = 5; + + uint8_t client_id; + int16_t slot_id; + int16_t amount; + uint8_t prefix; + int16_t item_id; +}; + +struct request_world_data { + static constexpr uint8_t packet_id = 6; +}; + +struct world_info { + static constexpr uint8_t packet_id = 7; + + // FIXME: i'm not sure this struct is correct + int32_t time; + uint8_t day_info; + uint8_t moon_phase; + int16_t max_tiles_x; + int16_t max_tiles_y; + int16_t spawn_x; + int16_t spawn_y; + int16_t world_surface; + int16_t rock_layer; + int32_t world_id; + std::string world_name; + uint8_t game_mode; + std::array world_unique_id; + std::array world_generator_version; + uint8_t moon_type; + std::array backgrounds; + uint8_t ice_back_style; + uint8_t jungle_back_style; + uint8_t hell_back_style; + float wind_speed_set; + uint8_t cloud_number; + std::array trees; + std::array tree_styles; + std::array cave_backs; + std::array cave_back_styles; + std::array tree_top_styles; + float rain; + bitset<104> flags1; + std::array ore_tiers_tiles; + int8_t invasion_type; + uint64_t lobby_id; + float sandstorm_severity; +}; + +struct request_tiles_at { + static constexpr uint8_t packet_id = 8; + int32_t spawn_x; + int32_t spawn_y; +}; + +struct statusbar_text { + static constexpr uint8_t packet_id = 9; + + int32_t status_max; + nstring status_text; + uint8_t server_flags; +}; + +struct send_tile_data { + static constexpr uint8_t packet_id = 10; + static constexpr packet_flag compressed {}; + + //FIXME: implement +}; + +struct spawn_player { + static constexpr uint8_t packet_id = 12; + + uint8_t client_id; + + int16_t spawn_x; + int16_t spawn_y; + int32_t respawn_timer; + uint16_t pve_death_count; + uint16_t pvp_death_count; + uint8_t spwan_context; +}; + +struct player_health { + static constexpr uint8_t packet_id = 16; + + uint8_t client_id; + uint16_t current; + uint16_t max; +}; + +struct request_password { + static constexpr uint8_t packet_id = 37; +}; + +struct send_password { + static constexpr uint8_t packet_id = 38; + + std::string password; +}; + +struct player_mana { + static constexpr uint8_t packet_id = 42; + + uint8_t client_id; + uint16_t current; + uint16_t max; +}; + +struct initial_spawn_player { + static constexpr uint8_t packet_id = 49; +}; + +struct update_player_buffs { + static constexpr uint8_t packet_id = 50; + + uint8_t client_id; + std::array buffs; +}; + +struct world_evil { + static constexpr uint8_t packet_id = 57; + + uint8_t good; + uint8_t evil; + uint8_t blood; +}; + +struct player_uuid { + static constexpr uint8_t packet_id = 68; + + std::string uuid; +}; + +struct npc_kill_count { + static constexpr uint8_t packet_id = 83; + + uint16_t npc_type; + uint32_t npc_kill_count; +}; + +struct tower_powers { + static constexpr uint8_t packet_id = 101; + uint16_t solar, nebula, vertex, stardust; +}; + +struct monster_types { + static constexpr uint8_t packet_id = 136; + + std::array, 2> types; +}; + +struct connection_completed { + static constexpr uint8_t packet_id = 129; +}; + +struct player_zones { + static constexpr uint8_t packet_id = 36; + + uint8_t client_id; + uint32_t zone_flags; + uint8_t zone_flags2; +}; + +struct player_loadout { + static constexpr uint8_t packet_id = 147; + + uint8_t client_id; + uint8_t index; + uint16_t hide_accessory; +}; + +struct damage_player { + static constexpr uint8_t packet_id = 117; + + uint8_t client_id; + death_reason reason; + int16_t damage; + uint8_t hit_direction; + uint8_t ty; + int8_t cooldown_counter; +}; + +template +struct raw { + static constexpr uint8_t packet_id = id; + std::vector data; +}; + +inline nstring operator""_ns(const char* str, std::size_t) +{ + return nstring(str); +} + +} +} diff --git a/include/util/bitset.hpp b/include/util/bitset.hpp new file mode 100644 index 0000000..8798a4a --- /dev/null +++ b/include/util/bitset.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +constexpr int bits_ceil(int bits) { + return bits / 8 + (bits % 8 > 0 ? 1 : 0); +} + +template +struct bitset : std::array { + using array_type = std::array; + +private: + struct bit_ref { + bitset &a; + int referenced_byte; + uint8_t referenced_bit_mask; + + bool operator=(bool b) { + if (b) + a.at(referenced_byte) |= referenced_bit_mask; + else + a.at(referenced_byte) &= ~referenced_bit_mask; + return b; + } + + constexpr operator bool() const { + return (a.at(referenced_byte) & referenced_bit_mask) > 0; + } + }; + +public: + template + constexpr bool operator [](T idx) const { + unsigned int i = (unsigned int)(idx); + assert(i < flags); + return (array_type::at(i/8) & (1 << (i%8))) > 0; + } + + template + bit_ref operator[](T idx) { + unsigned int i = (unsigned int)(idx); + assert(i < flags); + return bit_ref(*this, i/8, (1 << (i%8))); + } +}; \ No newline at end of file diff --git a/include/util/endian.hpp b/include/util/endian.hpp new file mode 100644 index 0000000..18e49f6 --- /dev/null +++ b/include/util/endian.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +template + requires(sizeof(T) <= 8) +T to_little(T x) +{ + if constexpr (std::endian::native == std::endian::little || sizeof(T) == 1) { + return x; + } + switch (sizeof(T)) { + case 2: + return __builtin_bswap16(x); + case 4: + return __builtin_bswap32(x); + case 8: + return __builtin_bswap64(x); + } +} \ No newline at end of file diff --git a/include/version.hpp b/include/version.hpp new file mode 100644 index 0000000..0371595 --- /dev/null +++ b/include/version.hpp @@ -0,0 +1,3 @@ +#pragma once + +constexpr auto max_buffs = 44; \ No newline at end of file diff --git a/include/world/world.hpp b/include/world/world.hpp new file mode 100644 index 0000000..1b2481b --- /dev/null +++ b/include/world/world.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace world { + +class world { + int32_t version; + +}; + +} \ No newline at end of file -- cgit v1.2.3