diff options
Diffstat (limited to 'serialize')
-rw-r--r-- | serialize/atlas-type.hpp | 8 | ||||
-rw-r--r-- | serialize/binary-serializer.cpp | 2 | ||||
-rw-r--r-- | serialize/binary-writer.hpp | 5 | ||||
-rw-r--r-- | serialize/binary-writer.inl | 12 | ||||
-rw-r--r-- | serialize/world-impl.hpp | 51 | ||||
-rw-r--r-- | serialize/world-reader.cpp | 26 | ||||
-rw-r--r-- | serialize/world-writer.cpp | 886 |
7 files changed, 416 insertions, 574 deletions
diff --git a/serialize/atlas-type.hpp b/serialize/atlas-type.hpp new file mode 100644 index 00000000..01b682c3 --- /dev/null +++ b/serialize/atlas-type.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace floormat::Serialize { + enum class atlas_type : uint8_t + { + none, ground, wall, object, + }; +} // namespace floormat::Serialize diff --git a/serialize/binary-serializer.cpp b/serialize/binary-serializer.cpp index ddd8b401..493fcf90 100644 --- a/serialize/binary-serializer.cpp +++ b/serialize/binary-serializer.cpp @@ -36,7 +36,7 @@ static_assert(std::is_same_v<test4&, decltype( std::declval<test4&>() << int() ) constexpr bool test5() { std::array<char, 4> bytes = {}; - auto w = binary_writer(bytes.begin()); + auto w = binary_writer(bytes.begin(), bytes.size()); w << (char)0; w << (char)1; w << (char)2; diff --git a/serialize/binary-writer.hpp b/serialize/binary-writer.hpp index 625bb53c..54e70a5c 100644 --- a/serialize/binary-writer.hpp +++ b/serialize/binary-writer.hpp @@ -7,14 +7,15 @@ namespace floormat::Serialize { template<std::output_iterator<char> It> struct binary_writer final { - explicit constexpr binary_writer(It it) noexcept; + explicit constexpr binary_writer(It it, size_t allocated_bytes) noexcept; template<serializable T> constexpr void write(T x) noexcept; constexpr void write_asciiz_string(StringView str) noexcept; constexpr size_t bytes_written() const noexcept { return _bytes_written; } + constexpr size_t bytes_allocated() const noexcept { return _bytes_allocated; } private: It it; - size_t _bytes_written; + size_t _bytes_written, _bytes_allocated; }; template<std::output_iterator<char> It, serializable T> diff --git a/serialize/binary-writer.inl b/serialize/binary-writer.inl index 0833a6f0..424e11e3 100644 --- a/serialize/binary-writer.inl +++ b/serialize/binary-writer.inl @@ -8,14 +8,19 @@ namespace floormat::Serialize { template<std::output_iterator<char> It> -constexpr binary_writer<It>::binary_writer(It it) noexcept : it{it}, _bytes_written{0} {} +constexpr binary_writer<It>::binary_writer(It it, size_t bytes_allocated) noexcept : + it{it}, + _bytes_written{0}, + _bytes_allocated{bytes_allocated} +{} template<std::output_iterator<char> It> template<serializable T> constexpr void binary_writer<It>::write(T x) noexcept { - _bytes_written += sizeof(T); constexpr size_t N = sizeof(T); + _bytes_written += N; + fm_assert(_bytes_written <= _bytes_allocated); const auto buf = std::bit_cast<std::array<char, N>, T>(maybe_byteswap(x)); for (auto i = 0uz; i < N; i++) *it++ = buf[i]; @@ -32,9 +37,10 @@ template<std::output_iterator<char> It> constexpr void binary_writer<It>::write_asciiz_string(StringView str) noexcept { //fm_debug_assert(str.flags() & StringViewFlag::NullTerminated); - fm_debug_assert(!str.find('\0')); + fm_assert(!str.find('\0')); const auto sz = str.size(); _bytes_written += sz + 1; + fm_assert(_bytes_written <= _bytes_allocated); for (auto i = 0uz; i < sz; i++) *it++ = str[i]; *it++ = '\0'; diff --git a/serialize/world-impl.hpp b/serialize/world-impl.hpp index 07b80514..18aec72a 100644 --- a/serialize/world-impl.hpp +++ b/serialize/world-impl.hpp @@ -1,15 +1,7 @@ -#ifndef FM_SERIALIZE_WORLD_IMPL -#error "not meant to be included directly" -#endif - #pragma once -#include "src/tile.hpp" -#include "src/pass-mode.hpp" -#include "src/rotation.hpp" -#include "src/object-type.hpp" -#include <bit> #include <cstdio> #include <concepts> +#include <Corrade/Containers/StringView.h> /* protocol changelog: * 1) Initial version. @@ -30,6 +22,7 @@ * 15) Add light alpha. * 16) One more bit for light falloff enum. * 17) Switch critter::offset_frac to unsigned. + * 20) Just rewrite the whole thing. */ namespace floormat { @@ -40,8 +33,8 @@ struct object_proto; namespace floormat::Serialize { using tilemeta = uint8_t; -using atlasid = uint16_t; -using chunksiz = uint16_t; +using atlasid = uint32_t; +using chunksiz = uint32_t; using proto_t = uint16_t; template<typename T> struct int_traits; @@ -51,32 +44,14 @@ template<std::signed_integral T> struct int_traits<T> { static constexpr T max = namespace { -#define file_magic ".floormat.save" - -constexpr inline proto_t proto_version = 19; - -constexpr inline size_t atlas_name_max = 128; -constexpr inline auto null_atlas = (atlasid)-1LL; - -constexpr inline size_t critter_name_max = 128; -constexpr inline size_t string_max = 512; - -constexpr inline proto_t min_proto_version = 1; -constexpr inline auto chunk_magic = (uint16_t)~0xc0d3; -constexpr inline auto scenery_magic = (uint16_t)~0xb00b; - -constexpr inline auto pass_mask = (1 << pass_mode_BITS)-1; - -template<typename T, size_t N, size_t off> -constexpr inline auto highbits = (T(1) << N)-1 << sizeof(T)*8-N-off; - -template<size_t N, std::unsigned_integral T = uint8_t> -constexpr T lowbits = N == sizeof(T)*8 ? (T)-1 : T((T{1} << N)-T{1}); - -constexpr inline uint8_t meta_short_scenery_bit = highbits<uint8_t, 1, 0>; -constexpr inline uint8_t meta_rotation_bits = highbits<uint8_t, rotation_BITS, 1>; -constexpr inline uint8_t scenery_id_flag_mask = meta_short_scenery_bit | meta_rotation_bits; -constexpr inline uint8_t scenery_id_max = int_traits<uint8_t>::max & ~scenery_id_flag_mask; +constexpr inline proto_t proto_version = 20; +constexpr inline proto_t min_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 string_max = 256uz; +constexpr inline auto null_atlas = (atlasid)-1; } // namespace @@ -103,7 +78,7 @@ 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) ::fclose(s); s = nullptr; } + void close() noexcept { if (s) std::fclose(s); s = nullptr; } private: FILE* s; }; diff --git a/serialize/world-reader.cpp b/serialize/world-reader.cpp index 4387acda..ab196f7e 100644 --- a/serialize/world-reader.cpp +++ b/serialize/world-reader.cpp @@ -1,7 +1,8 @@ #define FM_SERIALIZE_WORLD_IMPL #include "world-impl.hpp" -#include "binary-reader.inl" #include "src/world.hpp" +#if 0 +#include "binary-reader.inl" #include "src/scenery.hpp" #include "src/critter.hpp" #include "src/light.hpp" @@ -78,7 +79,12 @@ bool read_object_flags(binary_reader<T>& s, U& e) e.pass = pass_mode(flags & pass_mask); if (e.type != tag) fm_throw("invalid object type '{}'"_cf, (int)e.type); - if constexpr(tag == object_type::scenery) + if constexpr(tag == object_type::generic_scenery) + { + e.active = !!(flags & 1 << 2); + e.interactive = !!(flags & 1 << 4); + } + else if constexpr(tag == object_type::door) { e.active = !!(flags & 1 << 2); e.closing = !!(flags & 1 << 3); @@ -292,6 +298,9 @@ void reader_state::read_chunks(reader_t& s) SET_CHUNK_SIZE(); switch (type) { + case object_type::door { + ...; // todo + } case object_type::critter: { critter_proto proto; proto.offset = offset; @@ -337,7 +346,7 @@ void reader_state::read_chunks(reader_t& s) (void)e; break; } - case object_type::scenery: { + case object_type::generic_scenery: { atlasid id; id << s; bool exact; rotation r; @@ -548,3 +557,14 @@ world world::deserialize(StringView filename) } } // namespace floormat + +#endif + +namespace floormat { + +class world world::deserialize(StringView filename) +{ + fm_assert("todo" && false); +} + +} // namespace floormat diff --git a/serialize/world-writer.cpp b/serialize/world-writer.cpp index 649c1e9f..528ec332 100644 --- a/serialize/world-writer.cpp +++ b/serialize/world-writer.cpp @@ -1,623 +1,445 @@ -#define FM_SERIALIZE_WORLD_IMPL #include "world-impl.hpp" -#include "src/ground-atlas.hpp" -#include "src/wall-atlas.hpp" #include "binary-writer.inl" -#include "src/global-coords.hpp" -#include "src/chunk.hpp" -#include "src/world.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 "loader/scenery.hpp" -#include "src/anim-atlas.hpp" #include "src/light.hpp" -#include "compat/strerror.hpp" -#include <cerrno> -#include <concepts> + +#include <compare> +#include <memory> #include <vector> #include <algorithm> -#include <string_view> -#include <tsl/robin_map.h> -#include <Corrade/Containers/Array.h> -#include <Corrade/Containers/StringStlHash.h> #include <Corrade/Utility/Path.h> +#include <tsl/robin_map.h> -using namespace floormat; -using namespace floormat::Serialize; +#if 1 +#ifdef __CLION_IDE__ +#undef fm_assert +#define fm_assert(...) (void)(__VA_ARGS__) +#endif +#endif + +namespace floormat::Serialize { namespace { + struct string_container + { + StringView str; + bool operator==(const string_container&) const = default; -struct interned_atlas final { - StringView name; - atlasid index; - Vector2ub num_tiles; -}; + friend void swap(string_container& a, string_container& b) + { + auto tmp = a.str; + a.str = b.str; + b.str = tmp; + } + }; +} -struct interned_scenery { - const serialized_scenery* s; - atlasid index; - static_assert(sizeof index >= sizeof scenery::frame); -}; +} // namespace floormat::Serialize -struct scenery_pair { - const scenery_proto* s; - atlasid index; - bool exact_match; -}; +using floormat::Serialize::string_container; +using floormat::Hash::fnvhash_buf; -struct writer_state final { - writer_state(const world& world); - ArrayView<const char> serialize_world(); - fm_DECLARE_DEFAULT_MOVE_ASSIGNMENT_(writer_state); - fm_DECLARE_DEPRECATED_COPY_ASSIGNMENT(writer_state); - -private: - using writer_t = binary_writer<decltype(std::vector<char>{}.begin())>; - - atlasid intern_atlas(const void* ptr, StringView name, Vector2ub num_tiles); - scenery_pair intern_scenery(const scenery& sc, bool create); - uint32_t intern_string(StringView name); - - void serialize_scenery(const chunk& c, writer_t& s); - void serialize_chunk(const chunk& c, chunk_coords_ coord); - void serialize_atlases(); - void serialize_scenery_names(); - void serialize_strings(); - - void load_scenery_1(const serialized_scenery& s); - void load_scenery(); - - const world* _world; - std::vector<char> atlas_buf, scenery_buf, chunk_buf, file_buf, string_buf; - std::vector<std::vector<char>> chunk_bufs; - tsl::robin_map<const void*, interned_atlas> tile_images; - std::unordered_map<const void*, std::vector<interned_scenery>> scenery_map; - tsl::robin_map<StringView, uint32_t> string_map; - atlasid scenery_map_size = 0; +template<> struct std::hash<string_container> +{ + size_t operator()(const string_container& x) const noexcept + { + return fnvhash_buf(x.str.data(), x.str.size()); + } }; -constexpr auto tile_size = sizeof(tilemeta) + (sizeof(atlasid) + sizeof(variant_t)) * 3; -constexpr auto chunkbuf_size = sizeof(chunk_magic) + sizeof(chunk_coords_) + tile_size * TILE_COUNT + sizeof(uint32_t); -constexpr auto object_size = std::max({ sizeof(critter), sizeof(scenery), sizeof(light), }); -writer_state::writer_state(const world& world) : _world{&world} -{ - chunk_buf.reserve(chunkbuf_size); - chunk_bufs.reserve(world.chunks().size()); - atlas_buf.reserve(atlas_name_max * 64); - scenery_map.reserve(64); - string_map.reserve(64); -} +namespace floormat::Serialize { + +namespace { + +template<typename T> T& non_const(const T& value) { return const_cast<T&>(value); } +template<typename T> T& non_const(T& value) = delete; +template<typename T> T& non_const(T&& value) = delete; +template<typename T> T& non_const(const T&& value) = delete; -uint32_t writer_state::intern_string(StringView name) +constexpr size_t vector_initial_size = 128, hash_initial_size = vector_initial_size*2; + +template<typename T> +struct magic { - auto [kv, fresh] = string_map.try_emplace(name, (uint32_t)string_map.size()); - return kv->second; -} + using type = T; + uint16_t magic; +}; -atlasid writer_state::intern_atlas(const void* ptr, StringView name, Vector2ub num_tiles) +struct buffer { - fm_debug_assert(ptr != nullptr); - if (auto it = tile_images.find(ptr); it != tile_images.end()) - return it->second.index; - else + std::unique_ptr<char[]> 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<char[]>(len)}, + size{len} { - auto aid = (atlasid)tile_images.size(); - tile_images[ptr] = { name, aid, num_tiles }; - return aid; + std::memset(&data[0], 0xfe, size); } -} +}; -void writer_state::load_scenery_1(const serialized_scenery& s) +struct serialized_atlas { - const void* const ptr = s.proto.atlas.get(); - fm_debug_assert(ptr != nullptr); - if (auto it = scenery_map.find(ptr); it == scenery_map.end()) - scenery_map[ptr] = { { &s, null_atlas } }; - else - { - auto& vec = scenery_map[ptr]; - for (const auto& x : vec) - if (s.proto.frame == x.s->proto.frame) - return; - vec.push_back({ &s, null_atlas }); - } -} + buffer buf; + const void* atlas; + atlas_type type; +}; -void writer_state::load_scenery() +struct serialized_chunk { - for (const auto& s : loader.sceneries()) - load_scenery_1(s); -} + buffer buf{}; + const chunk* c; +}; -scenery_pair writer_state::intern_scenery(const scenery& sc, bool create) +template<typename Derived> +struct visitor_ { - auto s = scenery_proto(sc); - const void* const ptr = s.atlas.get(); - fm_debug_assert(ptr != nullptr); - auto it = scenery_map.find(ptr); - fm_assert(it != scenery_map.end() && !it->second.empty()); - auto& vec = it->second; - interned_scenery *ret = nullptr, *ret2 = nullptr; - for (interned_scenery& x : vec) + template<typename T, typename F> + CORRADE_ALWAYS_INLINE void do_visit_nonconst(const T& value, F&& fun) { - const auto& proto = x.s->proto; - fm_assert(s.type == proto.type); - fm_assert(s.sc_type == proto.sc_type); - s.r = proto.r; - s.interactive = proto.interactive; - s.active = proto.active; - s.closing = proto.closing; - s.pass = proto.pass; - if (s == proto) - { - if (x.index != null_atlas) - return { &x.s->proto, x.index, true }; - else - ret = &x; - } - if (x.index != null_atlas) - ret2 = &x; + do_visit(non_const(value), fun); } - if (ret) + template<typename T, typename F> + CORRADE_ALWAYS_INLINE void do_visit(T&& value, F&& fun) { - ret->index = scenery_map_size++; - return { &ret->s->proto, ret->index, true }; + static_cast<Derived&>(*this).visit(value, fun); } - else if (create) + + template<typename T, typename F> + requires (std::is_arithmetic_v<T> && std::is_fundamental_v<T>) + void visit(T& x, F&& f) { - if (ret2) - return { &ret2->s->proto, ret2->index, false }; - else - { - fm_assert(vec[0].index == null_atlas); - return { &vec[0].s->proto, vec[0].index = scenery_map_size++, false }; - } + f(x); } - else - return {}; -} -template<typename T, object_subtype U> -void write_object_flags(binary_writer<T>& s, const U& e) -{ - uint8_t flags = 0; - auto pass = std::to_underlying(e.pass); - fm_assert((pass & pass_mask) == pass); - flags |= pass; - constexpr auto tag = object_type_<U>::value; - if (e.type_of() != tag) - fm_abort("invalid object type '%d'", (int)e.type_of()); - if constexpr(tag == object_type::scenery) + template<typename T, size_t N, typename F> + void visit(Math::Vector<N, T>& x, F&& f) { - flags |= (1 << 2) * e.active; - flags |= (1 << 3) * e.closing; - flags |= (1 << 4) * e.interactive; + for (uint32_t i = 0; i < N; i++) + do_visit(x.data()[i], f); } - else if constexpr(tag == object_type::critter) + +#if 0 + template<typename F> + void visit(StringView str, F&& f) { - flags |= (1 << 2) * e.playable; + f(str); } - else - static_assert(tag == object_type::none); - flags |= (1 << 7) * (e.frame <= 0xff); - s << flags; -} - -void writer_state::serialize_atlases() -{ - fm_assert(tile_images.size() < int_traits<atlasid>::max); - const auto sz = (atlasid)tile_images.size(); - const auto atlasbuf_size = sizeof(sz) + atlas_name_max*sz; - atlas_buf.resize(atlasbuf_size); - auto s = binary_writer{atlas_buf.begin()}; - fm_assert(sz <= int_traits<atlasid>::max); - - s << sz; - - std::vector<interned_atlas> atlases; - atlases.reserve(tile_images.size()); - - for (const auto& [_, t] : tile_images) - atlases.push_back(t); - std::sort(atlases.begin(), atlases.end(), [](const auto& a, const auto& b) { - return a.index < b.index; - }); +#endif - for (const auto& [name, aid, num_tiles] : atlases) + template<typename E, typename F> + requires std::is_enum_v<E> + void visit(E& x, F&& f) { - const auto namesiz = name.size(); - fm_debug_assert(s.bytes_written() + namesiz < atlasbuf_size); - fm_assert(namesiz < atlas_name_max); - s << num_tiles[0]; s << num_tiles[1]; - s.write_asciiz_string(name); + auto* ptr = const_cast<std::underlying_type_t<E>*>(reinterpret_cast<const std::underlying_type_t<E>*>(&x)); + do_visit(*ptr, f); } - atlas_buf.resize(s.bytes_written()); - fm_assert(s.bytes_written() <= atlasbuf_size); -} -constexpr auto atlasbuf_size0 = sizeof(atlasid) + sizeof(scenery); -constexpr auto atlasbuf_size1 = sizeof(uint8_t) + atlasbuf_size0*int_traits<uint8_t>::max + atlas_name_max; - -void writer_state::serialize_scenery_names() -{ - const size_t sz = scenery_map_size; - fm_assert(sz == (atlasid)sz); - std::vector<interned_scenery> vec; vec.reserve(scenery_map_size); - for (const auto& x : scenery_map) - for (const auto& s : x.second) - if (s.index != null_atlas) - vec.push_back(s); - fm_assert(sz == vec.size()); - - std::sort(vec.begin(), vec.end(), [](const interned_scenery& a, const interned_scenery& b) { - auto a_ = std::string_view{a.s->name.data(), a.s->name.size()}, - b_ = std::string_view{b.s->name.data(), b.s->name.size()}; - auto cmp = a_ <=> b_; - if (cmp == std::strong_ordering::equal) - return a.index < b.index; - else - return cmp == std::strong_ordering::less; - }); - - const auto atlasbuf_size = sizeof(uint16_t) + sizeof(sz) + atlasbuf_size1*sz; - scenery_buf.resize(atlasbuf_size); - - auto s = binary_writer{scenery_buf.begin()}; - - s << uint16_t{scenery_magic}; - s << (atlasid)sz; - - StringView last; - for (auto i = 0uz; i < sz; i++) + template<typename F> + void visit(object& obj, F&& f) { - fm_debug_assert(s.bytes_written() + atlasbuf_size1 < atlasbuf_size); - const auto& [sc, idx] = vec[i]; - if (sc->name != last) + do_visit(obj.id, f); + do_visit(obj.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 (obj.type) { - fm_assert(sc->name.size() < atlas_name_max); - last = sc->name; - auto num = 1uz; - for (auto j = i+1; j < sz && vec[j].s->name == sc->name; j++) - num++; - fm_assert(num < int_traits<uint8_t>::max); - s << (uint8_t)num; - fm_assert(sc->name.size() < atlas_name_max); - s.write_asciiz_string(sc->name); + case object_type::critter: do_visit(static_cast<critter&>(obj), f); return; + case object_type::generic_scenery: do_visit(static_cast<scenery&>(obj), f); return; + case object_type::light: do_visit(static_cast<light&>(obj), f); return; + case object_type::door: do_visit(static_cast<door&>(obj), f); return; + case object_type::COUNT: + case object_type::none: + break; } - s << idx; - write_object_flags(s, sc->proto); - if (sc->proto.frame <= 0xff) - s << (uint8_t)sc->proto.frame; - else - s << sc->proto.frame; + fm_abort("invalid object type '%d'", (int)obj.type); } - scenery_buf.resize(s.bytes_written()); -} + template<typename F> + void visit(tile_ref c, F&& f) + { + do_visit(c.ground(), f); + do_visit(c.wall_north(), f); + do_visit(c.wall_west(), f); + } +}; -void writer_state::serialize_strings() +struct writer final : visitor_<writer> { - static_assert(critter_name_max <= string_max); - auto len = 0uz; + const world& w; - Array<StringView> sorted_strings{string_map.size()}; - for (auto [s, i] : string_map) - sorted_strings[i] = s; + std::vector<StringView> string_array{}; + tsl::robin_map<string_container, uint32_t> string_map{hash_initial_size}; - for (const auto& k : sorted_strings) - { - fm_assert(k.size()+1 < string_max); - len += k.size()+1; - } - string_buf.resize(sizeof(uint32_t) + len); - auto s = binary_writer{string_buf.begin()}; - s << (uint32_t)sorted_strings.size(); - for (const auto& k : sorted_strings) + std::vector<serialized_atlas> atlas_array{}; + tsl::robin_map<const void*, uint32_t> atlas_map{hash_initial_size}; + + std::vector<serialized_chunk> chunk_array{vector_initial_size}; + + buffer header_buf{}; + + struct size_counter { - fm_assert(k.size() < string_max); - s.write_asciiz_string(k); - } - fm_assert(s.bytes_written() == sizeof(uint32_t) + len); - fm_assert(s.bytes_written() == string_buf.size()); -} + size_t& size; -void writer_state::serialize_scenery(const chunk& c, writer_t& s) -{ - constexpr auto def_char_bbox_size = Vector2ub(iTILE_SIZE2); // copied from character_proto + template<typename T> + requires (std::is_arithmetic_v<T> && std::is_fundamental_v<T>) + void operator()(T) { size += sizeof(T); } + }; - const auto object_count = (uint32_t)c.objects().size(); - s << object_count; - fm_assert(object_count == c.objects().size()); - for (const auto& e_ : c.objects()) + struct byte_writer { - const auto& e = *e_; - fm_assert(s.bytes_written() + object_size <= chunk_buf.size()); - object_id oid = e.id; - fm_assert((oid & lowbits<collision_data_BITS, object_id>) == e.id); - const auto type = e.type(); - s << oid; - s << std::to_underlying(type); - const auto local = e.coord.local(); - s << local.to_index(); - s << e.offset[0]; - s << e.offset[1]; - - constexpr auto write_bbox = [](auto& s, const auto& e) { - s << e.bbox_offset[0]; - s << e.bbox_offset[1]; - s << e.bbox_size[0]; - s << e.bbox_size[1]; - }; - switch (type) + binary_writer<char*>& s; + + template<typename T> + requires (std::is_fundamental_v<T> && std::is_arithmetic_v<T>) + void operator()(T value) { - default: - fm_abort("invalid object type '%d'", (int)type); - case object_type::critter: { - const auto& C = static_cast<const critter&>(e); - uint8_t id = 0; - const auto sc_exact = - C.bbox_offset.isZero() && - C.bbox_size == def_char_bbox_size; - id |= meta_short_scenery_bit * sc_exact; - id |= static_cast<decltype(id)>(C.r) << sizeof(id)*8-1-rotation_BITS; - s << id; - write_object_flags(s, C); - if (C.frame <= 0xff) - s << (uint8_t)C.frame; - else - s << C.frame; - s << C.offset_frac[0]; - s << C.offset_frac[1]; - fm_assert(C.name.size() < critter_name_max); - s << intern_string(C.name); - if (!sc_exact) - write_bbox(s, C); - break; - } - case object_type::scenery: { - const auto& sc = static_cast<const scenery&>(e); - auto [ss, img_s, sc_exact] = intern_scenery(sc, true); - sc_exact = sc_exact && - sc.bbox_offset == ss->bbox_offset && sc.bbox_size == ss->bbox_size && - sc.pass == ss->pass && sc.sc_type == ss->sc_type && - sc.active == ss->active && sc.closing == ss->closing && - sc.interactive == ss->interactive && - sc.delta == 0 && sc.frame == ss->frame; - fm_assert(img_s != null_atlas); - s << img_s; - uint8_t bits = 0; - bits |= meta_short_scenery_bit * sc_exact; - bits |= std::to_underlying(sc.r) << sizeof(bits)*8-1-rotation_BITS; - s << bits; - if (!sc_exact) - { - write_object_flags(s, sc); - fm_assert(sc.active || sc.delta == 0); - if (sc.frame <= 0xff) - s << (uint8_t)sc.frame; - else - s << sc.frame; - write_bbox(s, sc); - if (sc.active) - s << sc.delta; - } - break; - } - case object_type::light: { - const auto& L = static_cast<const light&>(e); - const auto exact = L.frame == 0 && L.pass == pass_mode::pass && - L.bbox_offset.isZero() && L.bbox_size.isZero(); - { - fm_assert(L.r < rotation_COUNT); - fm_assert(L.falloff < light_falloff_COUNT); - uint8_t flags = 0; - flags |= (uint8_t)exact; // 1 bit - flags |= ((uint8_t)L.r & lowbits<rotation_BITS>) << 1; // 3 input_bits - flags |= ((uint8_t)L.falloff & lowbits<light_falloff_BITS>) << 4; // 2 input_bits - flags |= (uint8_t)!!L.enabled << 7; // 1 bit - s << flags; - } - { - s << L.max_distance; - for (auto i = 0uz; i < 4; i++) - s << L.color[i]; - } - if (!exact) - { - fm_assert(L.frame < (1 << 14)); - fm_assert(L.pass < pass_mode_COUNT); - uint16_t frame = 0; - frame |= L.frame; - frame |= uint16_t(L.pass) << 14; - s << frame; - write_bbox(s, L); - } - break; + s << value; } - } - } -} - -void writer_state::serialize_chunk(const chunk& c, chunk_coords_ coord) -{ - fm_assert(chunk_buf.empty()); - const auto es_size = sizeof(uint32_t) + object_size*c.objects().size(); - chunk_buf.resize(std::max(chunk_buf.size(), chunkbuf_size + es_size)); - - auto s = binary_writer{chunk_buf.begin()}; + }; - s << chunk_magic << coord.x << coord.y; - fm_assert(coord.z >= chunk_z_min && coord.z <= chunk_z_max); - s << coord.z; + using visitor_<writer>::visit; - for (auto i = 0uz; i < TILE_COUNT; i++) + template<typename F> + void intern_atlas_(void* atlas, atlas_type type, F&& f) { - const tile_proto x = c[i]; - const auto ground = x.ground(); - const auto wall_north = x.wall_north(), wall_west = x.wall_west(); - //const auto scenery = x.scenery_frame; - - fm_debug_assert(s.bytes_written() + tile_size <= chunkbuf_size); - - auto img_g = ground.atlas ? intern_atlas(&*ground.atlas, ground.atlas->name(), ground.atlas->num_tiles2()) : null_atlas; - auto img_n = wall_north ? intern_atlas(&*wall_north.atlas, wall_north.atlas->name(), {0xff, 0xff}) : null_atlas; - auto img_w = wall_west ? intern_atlas(&*wall_west.atlas, wall_west.atlas->name(), {0xff, 0xff}) : null_atlas; + do_visit(atlas_magic, f); + do_visit(type, f); - fm_assert(!ground.atlas || ground.variant < ground.atlas->num_tiles()); + StringView name; - if (img_g == null_atlas && img_n == null_atlas && img_w == null_atlas) + switch (type) { - size_t j, max = std::min(TILE_COUNT, i + 0x80); - for (j = i+1; j < max; j++) - { - auto tile = c[j]; - if (tile.ground_atlas || tile.wall_north_atlas || tile.wall_west_atlas) - break; - } - j -= i + 1; - fm_assert(j == (j & 0x7fuz)); - i += j; - tilemeta flags = meta_rle | (tilemeta)j; - s << flags; - - continue; + case atlas_type::ground: name = reinterpret_cast<const ground_atlas*>(atlas)->name(); goto ok; + case atlas_type::wall: name = reinterpret_cast<const wall_atlas*>(atlas)->name(); goto ok; + case atlas_type::object: name = reinterpret_cast<const anim_atlas*>(atlas)->name(); goto ok; + case atlas_type::none: break; } + fm_abort("invalid atlas type '%d'", (int)type); - tilemeta flags = {}; - flags |= meta_ground * (img_g != null_atlas); - flags |= meta_wall_n * (img_n != null_atlas); - flags |= meta_wall_w * (img_w != null_atlas); - s << flags; +ok: do_visit(intern_string(name), f); + } - if (img_g != null_atlas) + [[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) { - s << img_g; - s << ground.variant; + fm_debug_assert(kv.value() != (uint32_t)-1); + return kv->second; } - if (img_n != null_atlas) + else { - s << img_n; - s << wall_north.variant; + size_t len = 0; + intern_atlas_(atlas, type, size_counter{len}); + fm_assert(len > 0); + + buffer buf{len}; + binary_writer<char*> 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}; } - if (img_w != null_atlas) + } + + 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 { - s << img_w; - s << wall_west.variant; + auto id = (uint32_t)string_array.size(); + string_array.emplace_back(str); + kv.value() = id; + fm_assert(id != null_atlas); + return atlasid{id}; } } - serialize_scenery(c, s); + template<typename F> + void serialize_objects_(chunk& c, F&& f) + { + f((uint32_t)c.objects().size()); - const auto nbytes = s.bytes_written(); - fm_assert(nbytes <= chunkbuf_size); + for (const std::shared_ptr<object>& obj : c.objects()) + { + fm_assert(obj != nullptr); + do_visit(object_magic, f); + do_visit(*obj, f); + } + } - chunk_bufs.emplace_back(chunk_buf.cbegin(), chunk_buf.cbegin() + ptrdiff_t(nbytes)); - chunk_buf.clear(); -} + template<typename F> + 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); + } -#ifdef __GNUG__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#elif defined _MSC_VER -#pragma warning(push) -#pragma warning(disable : 4996) -#endif + void serialize_chunk_(chunk& c) + { + size_t len = 0; + { + auto ctr = size_counter{len}; + do_visit(chunk_magic, ctr); + do_visit(c.coord(), ctr); + fm_assert(len > 0); + for (uint32_t i = 0; i < TILE_COUNT; i++) + serialize_tile_(c[i], ctr); + serialize_objects_(c, ctr); + } -ArrayView<const char> writer_state::serialize_world() -{ - fm_assert(_world != nullptr); - load_scenery(); + buffer buf{len}; + { + binary_writer<char*> s{&buf.data[0], buf.size}; + byte_writer b{s}; + do_visit(chunk_magic, b); + do_visit(c.coord(), b); + for (uint32_t i = 0; i < TILE_COUNT; i++) + serialize_tile_(c[i], b); + serialize_objects_(c, b); + fm_assert(s.bytes_written() == s.bytes_allocated()); + } + chunk_array.emplace_back(std::move(buf), &c); + } - for (const auto& [_, c] : _world->chunks()) + template<typename F> + void serialize_header_(F&& f) { - for (const auto& e_ : c.objects()) + fm_assert(header_buf.empty()); + for (char c : file_magic) + f(c); + 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_world() + { + fm_assert(string_array.empty()); + fm_assert(atlas_array.empty()); + fm_assert(chunk_array.empty()); + fm_assert(header_buf.empty()); + + struct pair { chunk_coords_ coord; chunk* c; }; + std::vector<pair> chunks; + chunks.reserve(w.chunks().size()); + + for (auto& [coord, c] : w.chunks()) + chunks.push_back(pair{coord, &non_const(c)}); + std::sort(chunks.begin(), chunks.end(), [](const auto& at, const auto& bt) { + auto a = at.coord, b = bt.coord; + return std::tuple{a.z, a.y, a.x} <=> std::tuple{b.z, b.y, b.x} == std::strong_ordering::less; + }); + + for (auto [coord, c] : chunks) + serialize_chunk_(*c); + + size_t len = 0; + { + fm_assert(header_buf.empty()); + serialize_header_(size_counter{len}); + fm_assert(len > 0); + } + buffer hdr{len}; { - const auto& e = *e_; - switch (e.type()) - { - case object_type::scenery: - intern_scenery(static_cast<const scenery&>(e), false); - break; - case object_type::critter: - case object_type::light: - break; - default: - fm_abort("invalid scenery type '%d'", (int)e.type()); - } + binary_writer<char*> s{&hdr.data[0], hdr.size}; + serialize_header_(byte_writer{s}); + fm_assert(s.bytes_written() == s.bytes_allocated()); } + header_buf = std::move(hdr); } - for (const auto& [pos, c] : _world->chunks()) + + template<typename F> + void visit(anim_atlas& a, F&& f) { -#ifndef FM_NO_DEBUG - if (c.empty(true)) - fm_warn("chunk %hd:%hd is empty", pos.x, pos.y); -#endif - serialize_chunk(c, pos); + atlasid id = intern_atlas(&a, atlas_type::object); + do_visit(id, f); } - serialize_atlases(); - serialize_scenery_names(); - serialize_strings(); - - using proto_t = std::decay_t<decltype(proto_version)>; - fm_assert(_world->size() <= int_traits<chunksiz>::max); - - const auto len = fm_begin( - auto len = 0uz; - len += std::size(file_magic)-1; - len += sizeof(proto_t); - len += atlas_buf.size(); - len += scenery_buf.size(); - len += string_buf.size(); - len += sizeof(object_id); - len += sizeof(chunksiz); - for (const auto& buf : chunk_bufs) - len += buf.size(); - return len; - ); - file_buf.resize(len); - auto bytes_written = 0uz; - auto it = file_buf.begin(); - const auto copy = [&](const auto& in) { - auto len1 = std::distance(std::cbegin(in), std::cend(in)), - len2 = std::distance(it, file_buf.end()); - fm_assert(len1 <= len2); - it = std::copy(std::cbegin(in), std::cend(in), it); - bytes_written += (size_t)len1; - }; - const auto copy_int = [&]<typename T>(const T& value) { - union { T x; char bytes[sizeof x]; } c = {.x = maybe_byteswap(value)}; - copy(c.bytes); - }; - copy(Containers::StringView{file_magic, std::size(file_magic)-1}); - copy_int((proto_t)proto_version); - copy(atlas_buf); - copy(scenery_buf); - copy(string_buf); - copy_int(_world->object_counter()); - copy_int((chunksiz)_world->size()); - for (const auto& buf : chunk_bufs) - copy(buf); - fm_assert(file_buf.size() == bytes_written); - fm_assert(len == bytes_written); - _world = nullptr; - return {file_buf.data(), file_buf.size()}; -} -#ifdef __GNUG__ -#pragma GCC diagnostic pop -#elif defined _MSC_VER -#pragma warning(pop) -#endif + template<typename F> void visit(const chunk_coords_& coord, F&& f) + { + f(coord.x); + f(coord.y); + f(coord.z); + } + + template<typename F> void visit(const local_coords& pt, F&& f) + { + f(pt.to_index()); + } +}; + +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); @@ -629,13 +451,23 @@ void world::serialize(StringView filename) int error = errno; fm_abort("fopen(\"%s\", \"w\"): %s", filename.data(), get_error_string(errbuf, error).data()); } - writer_state s{*this}; - const auto array = s.serialize_world(); - if (auto len = ::fwrite(array.data(), array.size(), 1, file); len != 1) { - int error = errno; - fm_abort("fwrite: %s", get_error_string(errbuf, error).data()); + struct writer writer{.w = *this}; + const bool is_empty = chunks().empty(); + writer.serialize_world(); + if (!is_empty) + { + fm_assert(!writer.header_buf.empty()); + fm_assert(!writer.atlas_array.empty()); + fm_assert(!writer.atlas_map.empty()); + fm_assert(!writer.string_array.empty()); + fm_assert(!writer.string_map.empty()); + fm_assert(!writer.chunk_array.empty()); + } + my_fwrite(file, writer.header_buf, errbuf); + } + if (int ret = ::fflush(file); ret != 0) { int error = errno; |