#include "binary-writer.inl" #include "compat/defs.hpp" #include "compat/strerror.hpp" #include "compat/int-hash.hpp" #include "loader/loader.hpp" #include "src/world.hpp" #include "atlas-type.hpp" #include "src/anim-atlas.hpp" #include "src/ground-atlas.hpp" #include "src/wall-atlas.hpp" #include "src/scenery.hpp" #include "src/critter.hpp" #include "src/light.hpp" #include #include #include #include #include #include #include #if 1 #ifdef __CLION_IDE__ #undef fm_assert #define fm_assert(...) (void)(__VA_ARGS__) #endif #endif namespace floormat::Serialize { namespace { using tilemeta = uint8_t; using atlasid = uint32_t; using chunksiz = uint32_t; using proto_t = uint32_t; constexpr inline proto_t proto_version = 20; constexpr inline auto file_magic = ".floormat.save"_s; constexpr inline auto chunk_magic = (uint16_t)0xdead; constexpr inline auto object_magic = (uint16_t)0xb00b; constexpr inline auto atlas_magic = (uint16_t)0xbeef; constexpr inline auto null_atlas = (atlasid)-1; struct FILE_raii final { FILE_raii(FILE* s) noexcept : s{s} {} ~FILE_raii() noexcept { close(); } operator FILE*() noexcept { return s; } void close() noexcept { if (s) std::fclose(s); s = nullptr; } private: FILE* s; }; using floormat::Hash::fnvhash_buf; template T& non_const(const T& value) { return const_cast(value); } template T& non_const(T& value) = delete; template T& non_const(T&& value) = delete; template T& non_const(const T&& value) = delete; constexpr size_t vector_initial_size = 128, hash_initial_size = vector_initial_size*2; struct buffer { std::unique_ptr data; size_t size; bool empty() const { return size == 0; } buffer() : data{nullptr}, size{0} {} buffer(size_t len) : // todo use allocator data{std::make_unique(len)}, size{len} { #if !fm_ASAN std::memset(&data[0], 0xfe, size); #endif } }; struct serialized_atlas { buffer buf; const void* atlas; atlas_type type; }; struct serialized_chunk { buffer buf{}; chunk* c; }; template struct visitor_ { template CORRADE_ALWAYS_INLINE void do_visit_nonconst(const T& value, F&& fun) { do_visit(non_const(value), fun); } template CORRADE_ALWAYS_INLINE void do_visit(T&& value, F&& fun) { static_cast(*this).visit(value, fun); } template requires (std::is_arithmetic_v && std::is_fundamental_v) static void visit(T& x, F&& f) { f(x); } template void visit(Math::Vector& x, F&& f) { for (uint32_t i = 0; i < N; i++) do_visit(x.data()[i], f); } template requires std::is_enum_v void visit(E& x, F&& f) { auto* ptr = const_cast*>(reinterpret_cast*>(&x)); do_visit(*ptr, f); } template void visit(object& obj, F&& f) { auto type = obj.type(); do_visit(obj.id, f); do_visit(type, f); fm_assert(obj.atlas); do_visit(*obj.atlas, f); //do_visit(*obj.c, f); do_visit(obj.coord.local(), f); do_visit_nonconst(obj.offset, f); do_visit_nonconst(obj.bbox_offset, f); do_visit_nonconst(obj.bbox_size, f); do_visit_nonconst(obj.delta, f); do_visit_nonconst(obj.frame, f); do_visit_nonconst(obj.r, f); do_visit_nonconst(obj.pass, f); switch (type) { case object_type::critter: do_visit(static_cast(obj), f); return; case object_type::scenery: do_visit(static_cast(obj), f); return; case object_type::light: do_visit(static_cast(obj), f); return; //case object_type::door: do_visit(static_cast(obj), f); return; case object_type::COUNT: case object_type::none: break; } fm_abort("invalid object type '%d'", (int)obj.type()); } template void visit(tile_ref c, F&& f) { do_visit(c.ground(), f); do_visit(c.wall_north(), f); do_visit(c.wall_west(), f); } }; struct string_hasher { inline size_t operator()(StringView s) const { return fnvhash_buf(s.data(), s.size()); } }; struct writer final : visitor_ { const world& w; std::vector string_array{}; tsl::robin_map string_map{hash_initial_size}; std::vector atlas_array{}; tsl::robin_map atlas_map{hash_initial_size}; std::vector chunk_array{}; buffer header_buf{}, string_buf{}; writer(const world& w) : w{w} { } // added to avoid spurious warning until GCC 14: warning: missing initializer for member :: struct size_counter { size_t& size; template requires (std::is_arithmetic_v && std::is_fundamental_v) void operator()(T) { size += sizeof(T); } }; struct byte_writer { binary_writer& s; template requires (std::is_fundamental_v && std::is_arithmetic_v) void operator()(T value) { s << value; } }; using visitor_::visit; template void visit(critter& obj, F&& f) { uint8_t flags = 0; flags |= (1 << 0) * obj.playable; do_visit(flags, f); do_visit(obj.name, f); do_visit(obj.offset_frac, f); } template void visit(scenery& obj, F&& f) { auto sc_type = obj.sc_type; do_visit(sc_type, f); uint8_t flags = 0; flags |= obj.active * (1 << 0); flags |= obj.closing * (1 << 1); flags |= obj.interactive * (1 << 2); do_visit(flags, f); } template void visit(light& obj, F&& f) { do_visit(obj.max_distance, f); do_visit(obj.color, f); auto falloff = obj.falloff; do_visit(falloff, f); uint8_t flags = 0; flags |= obj.enabled * (1 << 0); do_visit(flags, f); } template void intern_atlas_(void* atlas, atlas_type type, F&& f) { do_visit(atlas_magic, f); do_visit(type, f); StringView name; switch (type) { case atlas_type::ground: name = reinterpret_cast(atlas)->name(); goto ok; case atlas_type::wall: name = reinterpret_cast(atlas)->name(); goto ok; case atlas_type::object: name = reinterpret_cast(atlas)->name(); goto ok; case atlas_type::none: break; } fm_abort("invalid atlas type '%d'", (int)type); ok: do_visit(intern_string(name), f); } [[nodiscard]] atlasid intern_atlas(void* atlas, atlas_type type) { atlas_array.reserve(vector_initial_size); fm_assert(atlas != nullptr); auto [kv, fresh] = atlas_map.try_emplace(atlas, (uint32_t)-1); if (!fresh) { fm_debug_assert(kv.value() != (uint32_t)-1); return kv->second; } else { size_t len = 0; intern_atlas_(atlas, type, size_counter{len}); fm_assert(len > 0); buffer buf{len}; binary_writer s{&buf.data[0], buf.size}; intern_atlas_(atlas, type, byte_writer{s}); auto id = (uint32_t)atlas_array.size(); fm_assert(s.bytes_written() == s.bytes_allocated()); atlas_array.emplace_back(std::move(buf), atlas, type); kv.value() = id; fm_assert(id != null_atlas); return atlasid{id}; } } atlasid maybe_intern_atlas(void* atlas, atlas_type type) { if (!atlas) return null_atlas; else return intern_atlas(atlas, type); } atlasid intern_string(StringView str) { string_array.reserve(vector_initial_size); auto [kv, found] = string_map.try_emplace({str}, (uint32_t)-1); if (found) return kv.value(); else { auto id = (uint32_t)string_array.size(); string_array.emplace_back(str); kv.value() = id; fm_assert(id != null_atlas); return atlasid{id}; } } template void serialize_objects_(chunk& c, F&& f) { f((uint32_t)c.objects().size()); for (const std::shared_ptr& obj : c.objects()) { fm_assert(obj != nullptr); do_visit(object_magic, f); do_visit(*obj, f); } } template void serialize_tile_(tile_ref t, F&& f) { auto g = maybe_intern_atlas(t.ground_atlas().get(), atlas_type::ground), n = maybe_intern_atlas(t.wall_north_atlas().get(), atlas_type::wall), w = maybe_intern_atlas(t.wall_west_atlas().get(), atlas_type::wall); do_visit(g, f); do_visit(n, f); do_visit(w, f); if (g != null_atlas) do_visit(t.ground().variant, f); if (n != null_atlas) do_visit(t.wall_north().variant, f); if (w != null_atlas) do_visit(t.wall_west().variant, f); } void serialize_chunk_(chunk& c, buffer& buf) { const auto fn = [this](chunk& c, auto&& f) { do_visit(chunk_magic, f); do_visit(c.coord(), f); for (uint32_t i = 0; i < TILE_COUNT; i++) serialize_tile_(c[i], f); serialize_objects_(c, f); }; size_t len = 0; auto ctr = size_counter{len}; fn(c, ctr); fm_assert(len > 0); buf = buffer{len}; binary_writer s{&buf.data[0], buf.size}; byte_writer b{s}; fn(c, b); fm_assert(s.bytes_written() == s.bytes_allocated()); } template void serialize_header_(F&& f) { fm_assert(header_buf.empty()); for (char c : file_magic) f(c); f(proto_version); auto nstrings = (uint32_t)string_array.size(), natlases = (uint32_t)atlas_array.size(), nchunks = (uint32_t)chunk_array.size(); do_visit(nstrings, f); do_visit(natlases, f); do_visit(nchunks, f); } void serialize_strings_() { fm_assert(string_buf.empty()); size_t len = 0; len += sizeof uint32_t{}; for (const auto& s : string_array) len += s.size() + 1; buffer buf{len}; binary_writer b{&buf.data[0], buf.size}; b << (uint32_t)string_array.size(); for (const auto& s : string_array) b.write_asciiz_string(s); fm_assert(b.bytes_written() == b.bytes_allocated()); string_buf = std::move(buf); } void serialize_world() { fm_assert(string_array.empty()); fm_assert(atlas_array.empty()); fm_assert(chunk_array.empty()); fm_assert(header_buf.empty()); fm_assert(string_buf.empty()); for (auto& [coord, c] : non_const(w.chunks())) chunk_array.push_back({.c = &c }); std::sort(chunk_array.begin(), chunk_array.end(), [](const auto& c1, const auto& c2) { auto a = c1.c->coord(), b = c2.c->coord(); return std::tuple{a.z, a.y, a.x} <=> std::tuple{b.z, b.y, b.x} == std::strong_ordering::less; }); for (uint32_t i = 0; auto& [coord, c] : chunk_array) serialize_chunk_(*c, chunk_array[i++].buf); { size_t len = 0; serialize_header_(size_counter{len}); fm_assert(len > 0); buffer hdr{len}; binary_writer s{&hdr.data[0], hdr.size}; serialize_header_(byte_writer{s}); fm_assert(s.bytes_written() == s.bytes_allocated()); header_buf = std::move(hdr); } serialize_strings_(); } template void visit(anim_atlas& a, F&& f) { atlasid id = intern_atlas(&a, atlas_type::object); do_visit(id, f); } template void visit(const chunk_coords_& coord, F&& f) { f(coord.x); f(coord.y); f(coord.z); } template void visit(const local_coords& pt, F&& f) { f(pt.to_index()); } template void visit(StringView name, F&& f) { f(intern_string(name)); } }; void my_fwrite(FILE_raii& f, const buffer& buf, char(&errbuf)[128]) { auto len = ::fwrite(&buf.data[0], buf.size, 1, f); int error = errno; if (len != 1) fm_abort("fwrite: %s", get_error_string(errbuf, error).data()); } } // namespace } // namespace floormat::Serialize namespace floormat { void world::serialize(StringView filename) { using namespace floormat::Serialize; collect(true); char errbuf[128]; fm_assert(filename.flags() & StringViewFlag::NullTerminated); if (Path::exists(filename)) Path::remove(filename); FILE_raii file = ::fopen(filename.data(), "wb"); if (!file) { int error = errno; fm_abort("fopen(\"%s\", \"w\"): %s", filename.data(), get_error_string(errbuf, error).data()); } { struct writer writer{*this}; const bool is_empty = chunks().empty(); writer.serialize_world(); fm_assert(!writer.header_buf.empty()); if (!is_empty) { fm_assert(!writer.string_buf.empty()); fm_assert(!writer.string_array.empty()); fm_assert(!writer.string_map.empty()); fm_assert(!writer.atlas_array.empty()); fm_assert(!writer.atlas_map.empty()); fm_assert(!writer.chunk_array.empty()); } my_fwrite(file, writer.header_buf, errbuf); my_fwrite(file, writer.string_buf, errbuf); for (const auto& x : writer.atlas_array) { fm_assert(!x.buf.empty()); my_fwrite(file, x.buf, errbuf); } for (const auto& x : writer.chunk_array) { fm_assert(!x.buf.empty()); my_fwrite(file, x.buf, errbuf); } } if (int ret = ::fflush(file); ret != 0) { int error = errno; fm_abort("fflush: %s", get_error_string(errbuf, error).data()); } } } // namespace floormat