summaryrefslogtreecommitdiffhomepage
path: root/serialize/old-savegame.cpp
diff options
context:
space:
mode:
authorStanislaw Halik <sthalik@misaki.pl>2024-01-23 08:56:32 +0100
committerStanislaw Halik <sthalik@misaki.pl>2024-01-23 08:56:43 +0100
commit068e5dd08439932604e5ad5d1f22826107a3f308 (patch)
tree601f69ce0f3a273bfd6025a419bd4cdee606191c /serialize/old-savegame.cpp
parentb7779f7736afedd041f49bcdf36647687bf3d456 (diff)
a
Diffstat (limited to 'serialize/old-savegame.cpp')
-rw-r--r--serialize/old-savegame.cpp632
1 files changed, 632 insertions, 0 deletions
diff --git a/serialize/old-savegame.cpp b/serialize/old-savegame.cpp
new file mode 100644
index 00000000..9c5f7090
--- /dev/null
+++ b/serialize/old-savegame.cpp
@@ -0,0 +1,632 @@
+#include "binary-reader.inl"
+#include "src/world.hpp"
+#include "src/scenery.hpp"
+#include "src/critter.hpp"
+#include "src/light.hpp"
+#include "loader/loader.hpp"
+#include "loader/scenery.hpp"
+#include "src/ground-atlas.hpp"
+#include "src/anim-atlas.hpp"
+#include "src/chunk-scenery.hpp"
+#include "compat/strerror.hpp"
+#include "src/tile.hpp"
+#include "src/pass-mode.hpp"
+#include "src/rotation.hpp"
+#include "src/object-type.hpp"
+#include <bit>
+#include <cstdio>
+#include <cerrno>
+#include <cstring>
+#include <concepts>
+#include <memory>
+#include <vector>
+
+
+/* protocol changelog:
+ * 1) Initial version.
+ * 2) Tile atlas variant now always a uint8_t. Was uint16_t or uint8_t
+ * depending on value of the tile flag (1 << 6) which is now removed.
+ * 3) Serialize scenery. Tile flag (1 << 6) added.
+ * 4) Scenery dt now stored as fixed-point uint16_t.
+ * 5) Serialize scenery pixel offset.
+ * 6) Serialize scenery bboxes.
+ * 7) Serialize scenery bbox_size offset.
+ * 8) Entity subtypes.
+ * 9) Interned strings.
+ * 10) Chunk Z level.
+ * 11) RLE empty tiles.
+ * 12) Don't write object name twice.
+ * 13) Entity counter initialized to 1024.
+ * 14) Always store object offset, rework how sc_exact works.
+ * 15) Add light alpha.
+ * 16) One more bit for light falloff enum.
+ * 17) Switch critter::offset_frac to unsigned.
+ */
+
+namespace floormat {
+struct object;
+struct object_proto;
+} // namespace floormat
+
+namespace floormat::Serialize {
+
+using tilemeta = uint8_t;
+using atlasid = uint16_t;
+using chunksiz = uint16_t;
+using proto_t = uint16_t;
+
+template<typename T> struct int_traits;
+
+template<std::unsigned_integral T> struct int_traits<T> { static constexpr T max = T(-1); };
+template<std::signed_integral T> struct int_traits<T> { static constexpr T max = T(-1)&~(T(1) << sizeof(T)*8-1); };
+
+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;
+
+} // namespace
+
+template<typename T> concept object_subtype = std::is_base_of_v<object, T> || std::is_base_of_v<object_proto, T>;
+
+enum : tilemeta {
+ meta_ground = 1 << 2,
+ meta_wall_n = 1 << 3,
+ meta_wall_w = 1 << 4,
+ meta_rle = 1 << 7,
+
+ meta_short_atlasid_ = 1 << 5,
+ meta_short_variant_ = 1 << 6,
+ meta_scenery_ = 1 << 7,
+};
+
+} // namespace floormat::Serialize
+
+namespace floormat {
+
+namespace {
+
+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; }
+private:
+ FILE* s;
+};
+
+} // namespace
+
+} // namespace floormat
+
+namespace {
+
+using namespace floormat;
+using namespace floormat::Serialize;
+
+constexpr inline atlasid meta_short_scenery_bit_ = highbits<atlasid, 1, 0>;
+constexpr inline atlasid meta_rotation_bits_ = highbits<atlasid, rotation_BITS, 1>;
+constexpr inline atlasid scenery_id_flag_mask_ = meta_short_scenery_bit_ | meta_rotation_bits_;
+constexpr inline atlasid scenery_id_max_ = int_traits<atlasid>::max & ~scenery_id_flag_mask_;
+
+struct reader_state final {
+ explicit reader_state(world& world) noexcept;
+ void deserialize_world(ArrayView<const char> buf, proto_t proto);
+
+private:
+ using reader_t = binary_reader<decltype(ArrayView<const char>{}.cbegin())>;
+
+ StringView lookup_atlas(atlasid id);
+ const scenery_proto& lookup_scenery(atlasid id);
+ StringView lookup_string(uint32_t idx);
+ void read_atlases(reader_t& reader);
+ void read_sceneries(reader_t& reader);
+ void read_strings(reader_t& reader);
+ void read_chunks(reader_t& reader);
+ void read_old_scenery(reader_t& s, chunk_coords_ ch, size_t i);
+ void preload_chunks();
+
+ std::vector<String> strings;
+ std::vector<scenery_proto> sceneries;
+ std::vector<String> atlases;
+ world* _world;
+ uint16_t PROTO = proto_version;
+
+ Array<chunk::object_draw_order> draw_array;
+ Array<std::array<chunk::vertex, 4>> draw_vertexes;
+ Array<std::array<UnsignedShort, 6>> draw_indexes;
+};
+
+reader_state::reader_state(world& world) noexcept : _world{&world} {}
+
+void reader_state::read_atlases(reader_t& s)
+{
+ const auto N = s.read<atlasid>();
+ atlases.reserve(N);
+ for (atlasid i = 0; i < N; i++)
+ {
+ Vector2ub size;
+ size[0] << s;
+ size[1] << s;
+ const auto& [buf, len] = s.read_asciiz_string<atlas_name_max>();
+ atlases.push_back({buf, len});
+ }
+}
+
+template<typename T, object_subtype U>
+bool read_object_flags(binary_reader<T>& s, U& e)
+{
+ constexpr auto tag = object_type_<U>::value;
+ uint8_t flags; flags << s;
+ 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)
+ {
+ e.active = !!(flags & 1 << 2);
+ e.closing = !!(flags & 1 << 3);
+ e.interactive = !!(flags & 1 << 4);
+ }
+ else if constexpr(tag == object_type::critter)
+ {
+ e.playable = !!(flags & 1 << 2);
+ }
+ else
+ {
+ static_assert(tag == object_type::none);
+ static_assert(tag != object_type::none);
+ }
+ return flags & 1 << 7;
+}
+
+void reader_state::read_sceneries(reader_t& s)
+{
+ (void)loader.sceneries();
+
+ uint16_t magic; magic << s;
+ if (magic != scenery_magic)
+ fm_throw("bad scenery magic"_cf);
+ atlasid sz; sz << s;
+ fm_soft_assert(sz < scenery_id_max_);
+ sceneries.resize(sz);
+
+ auto i = 0uz;
+ while (i < sz)
+ {
+ uint8_t num; num << s;
+ fm_soft_assert(num > 0);
+ auto str = s.read_asciiz_string<atlas_name_max>();
+ auto sc = loader.scenery(str);
+ for (auto n = 0uz; n < num; n++)
+ {
+ atlasid id; id << s;
+ fm_soft_assert(id < sz);
+ fm_soft_assert(!sceneries[id]);
+ bool short_frame = read_object_flags(s, sc);
+ fm_debug_assert(sc.atlas != nullptr);
+ if (short_frame)
+ sc.frame = s.read<uint8_t>();
+ else
+ sc.frame << s;
+ fm_soft_assert(sc.frame < sc.atlas->info().nframes);
+ sceneries[id] = sc;
+ }
+ i += num;
+ }
+ fm_soft_assert(i == sz);
+}
+
+void reader_state::read_strings(reader_t& s)
+{
+ uint32_t size; size << s;
+ strings.reserve(size);
+ for (auto i = 0uz; i < size; i++)
+ {
+ auto str = s.read_asciiz_string<string_max>();
+ strings.emplace_back(StringView{str});
+ }
+}
+
+StringView reader_state::lookup_atlas(atlasid id)
+{
+ if (id < atlases.size())
+ return atlases[id];
+ else
+ fm_throw("no such atlas: '{}'"_cf, id);
+}
+
+const scenery_proto& reader_state::lookup_scenery(atlasid id)
+{
+ if (id < sceneries.size())
+ return sceneries[id];
+ else
+ fm_throw("no such scenery: '{}'"_cf, id);
+}
+
+StringView reader_state::lookup_string(uint32_t idx)
+{
+ fm_soft_assert(idx < strings.size());
+ return strings[idx];
+}
+
+#ifndef FM_NO_DEBUG
+# define SET_CHUNK_SIZE() do { nbytes_read = s.bytes_read() - nbytes_start; } while (false)
+#else
+# define SET_CHUNK_SIZE() void()
+#endif
+
+void reader_state::read_chunks(reader_t& s)
+{
+ Array<typename chunk::object_draw_order> array;
+ const auto N = s.read<chunksiz>();
+#ifndef FM_NO_DEBUG
+ [[maybe_unused]] size_t nbytes_read = 0;
+#endif
+
+ for (auto k = 0uz; k < N; k++)
+ {
+ const auto nbytes_start = s.bytes_read();
+
+ std::decay_t<decltype(chunk_magic)> magic;
+ magic << s;
+ if (magic != chunk_magic)
+ fm_throw("bad chunk magic"_cf);
+ chunk_coords_ ch;
+ ch.x << s;
+ ch.y << s;
+ if (PROTO >= 10) [[likely]]
+ ch.z << s;
+ auto& c = (*_world)[ch];
+ c.mark_modified();
+ for (auto i = 0uz; i < TILE_COUNT; i++)
+ {
+ SET_CHUNK_SIZE();
+ const tilemeta flags = s.read<tilemeta>();
+
+ if (PROTO >= 11) [[likely]]
+ if (flags & meta_rle)
+ {
+ auto j = flags & 0x7fuz;
+ i += j;
+ continue;
+ }
+
+ tile_ref t = c[i];
+ using uchar = uint8_t;
+ const auto make_atlas = [&]<typename T>() -> image_proto_<T> {
+ atlasid id;
+ if (PROTO < 8) [[unlikely]]
+ id = flags & meta_short_atlasid_ ? atlasid{s.read<uchar>()} : s.read<atlasid>();
+ else
+ id << s;
+ uint8_t v;
+ if (PROTO >= 2) [[likely]]
+ v << s;
+ else
+ v = flags & meta_short_variant_
+ ? s.read<uint8_t>()
+ : uint8_t(s.read<uint16_t>());
+ auto name = lookup_atlas(id);
+ if constexpr(std::is_same_v<ground_atlas, T>)
+ {
+ auto atlas = loader.ground_atlas(name, loader_policy::warn);
+ fm_soft_assert(v < atlas->num_tiles());
+ return { atlas, v };
+ }
+ else if (std::is_same_v<wall_atlas, T>)
+ {
+ auto atlas = loader.wall_atlas(name, loader_policy::warn);
+ return { atlas, v };
+ }
+ else
+ std::unreachable();
+ };
+ SET_CHUNK_SIZE();
+ //t.passability() = pass_mode(flags & pass_mask);
+ if (flags & meta_ground)
+ t.ground() = make_atlas.operator()<ground_atlas>();
+ if (flags & meta_wall_n)
+ t.wall_north() = make_atlas.operator()<wall_atlas>();
+ if (flags & meta_wall_w)
+ t.wall_west() = make_atlas.operator()<wall_atlas>();
+ if (PROTO >= 3 && PROTO < 8) [[unlikely]]
+ if (flags & meta_scenery_)
+ read_old_scenery(s, ch, i);
+ SET_CHUNK_SIZE();
+ }
+ uint32_t object_count = 0;
+ if (PROTO >= 8) [[likely]]
+ object_count << s;
+
+ SET_CHUNK_SIZE();
+
+ for (auto i = 0uz; i < object_count; i++)
+ {
+ object_id oid;
+ object_type type;
+ if (PROTO >= 18) [[likely]]
+ {
+ oid << s;
+ fm_soft_assert((oid & lowbits<collision_data_BITS, object_id>) == oid);
+ type = object_type(s.read<std::underlying_type_t<object_type>>());
+ fm_soft_assert(type < object_type::COUNT);
+ }
+ else
+ {
+ object_id _id; _id << s;
+ oid = _id & lowbits<60, object_id>;
+ fm_soft_assert(oid != 0);
+ type = object_type(_id >> 61);
+ }
+ const auto local = local_coords{s.read<uint8_t>()};
+
+ Vector2b offset;
+ if (PROTO >= 14) [[likely]]
+ {
+ offset[0] << s;
+ offset[1] << s;
+ }
+ constexpr auto read_bbox = [](auto& s, auto& e) {
+ s >> e.bbox_offset[0];
+ s >> e.bbox_offset[1];
+ s >> e.bbox_size[0];
+ s >> e.bbox_size[1];
+ };
+ SET_CHUNK_SIZE();
+ switch (type)
+ {
+ case object_type::critter: {
+ critter_proto proto;
+ proto.offset = offset;
+ uint8_t id; id << s;
+ proto.r = rotation(id >> sizeof(id)*8-1-rotation_BITS & rotation_MASK);
+ if (read_object_flags(s, proto))
+ proto.frame = s.read<uint8_t>();
+ else
+ proto.frame << s;
+ Vector2us offset_frac;
+ offset_frac[0] << s;
+ offset_frac[1] << s;
+ if (PROTO < 17) [[unlikely]]
+ offset_frac = {};
+ const bool exact = id & meta_short_scenery_bit_;
+ SET_CHUNK_SIZE();
+
+ if (PROTO >= 9) [[likely]]
+ {
+ uint32_t id; id << s;
+ auto name = lookup_string(id);
+ fm_soft_assert(name.size() < critter_name_max);
+ proto.name = name;
+ }
+ else
+ {
+ auto [buf, len] = s.read_asciiz_string<critter_name_max>();
+ auto name = StringView{buf, len};
+ proto.name = name;
+ }
+ if (!exact)
+ {
+ if (PROTO < 14) [[unlikely]]
+ {
+ s >> proto.offset[0];
+ s >> proto.offset[1];
+ }
+ read_bbox(s, proto);
+ }
+ SET_CHUNK_SIZE();
+ auto e = _world->make_object<critter, false>(oid, {ch, local}, proto);
+ e->offset_frac = offset_frac;
+ (void)e;
+ break;
+ }
+ case object_type::scenery: {
+ atlasid id; id << s;
+ bool exact;
+ rotation r;
+ if (PROTO >= 19) [[likely]]
+ {
+ uint8_t bits; bits << s;
+ exact = bits & meta_short_scenery_bit;
+ r = rotation(bits >> sizeof(bits)*8-1-rotation_BITS & rotation_MASK);
+ }
+ else
+ {
+ exact = id & meta_short_scenery_bit_;
+ r = rotation(id >> sizeof(id)*8-1-rotation_BITS & rotation_MASK);
+ id &= ~scenery_id_flag_mask_;
+ }
+ auto sc = lookup_scenery(id);
+ sc.offset = offset;
+ (void)sc.atlas->group(r);
+ sc.r = r;
+ if (!exact)
+ {
+ if (read_object_flags(s, sc))
+ sc.frame = s.read<uint8_t>();
+ else
+ sc.frame << s;
+ (void)sc.atlas->frame(sc.r, sc.frame);
+ if (PROTO < 14) [[unlikely]]
+ {
+ s >> sc.offset[0];
+ s >> sc.offset[1];
+ }
+ read_bbox(s, sc);
+ if (sc.active)
+ sc.delta << s;
+ }
+ auto e = _world->make_object<scenery, false>(oid, {ch, local}, sc);
+ (void)e;
+ break;
+ }
+ case object_type::light: {
+ light_proto proto;
+ proto.offset = offset;
+
+ uint8_t flags; flags << s;
+ const bool exact = flags & 1;
+ proto.r = rotation((flags >> 1) & lowbits<rotation_BITS>);
+ bool enabled;
+ if (PROTO >= 16) [[likely]]
+ {
+ proto.falloff = light_falloff((flags >> 4) & lowbits<light_falloff_BITS>);
+ enabled = (flags >> 7) & 1;
+ }
+ else
+ {
+ proto.falloff = light_falloff((flags >> 4) & lowbits<2>);
+ enabled = (flags >> 6) & 1;
+ }
+ s >> proto.max_distance;
+ for (auto i = 0uz; i < 3; i++)
+ s >> proto.color[i];
+ if (PROTO >= 15) [[likely]]
+ s >> proto.color[3];
+ if (!exact)
+ {
+ uint16_t frame; frame << s;
+ auto pass = pass_mode((frame >> 14) & lowbits<2>);
+ frame &= lowbits<14, uint16_t>;
+ proto.pass = pass;
+ proto.frame = frame;
+ read_bbox(s, proto);
+ }
+ SET_CHUNK_SIZE();
+ auto L = _world->make_object<light, false>(oid, {ch, local}, proto);
+ L->enabled = enabled;
+ (void)L;
+ break;
+ }
+ default:
+ fm_throw("invalid_object_type '{}'"_cf, (int)type);
+ }
+ }
+
+ SET_CHUNK_SIZE();
+ fm_assert(c.is_scenery_modified());
+ fm_assert(c.is_passability_modified());
+ c.sort_objects();
+ }
+}
+
+void reader_state::preload_chunks()
+{
+ for (auto& [coord, _] : _world->chunks())
+ {
+ auto* c = _world->at(coord);
+ fm_assert(c);
+ c->ensure_ground_mesh();
+ c->ensure_wall_mesh();
+ c->ensure_scenery_mesh({ draw_array, draw_vertexes, draw_indexes });
+ c->ensure_passability();
+ }
+}
+
+void reader_state::read_old_scenery(reader_t& s, chunk_coords_ ch, size_t i)
+{
+ atlasid id; id << s;
+ const bool exact = id & meta_short_scenery_bit_;
+ const auto r = rotation(id >> sizeof(id)*8-1-rotation_BITS & rotation_MASK);
+ id &= ~scenery_id_flag_mask_;
+ auto sc = lookup_scenery(id);
+ (void)sc.atlas->group(r);
+ sc.r = r;
+ if (!exact)
+ {
+ if (read_object_flags(s, sc))
+ sc.frame = s.read<uint8_t>();
+ else
+ sc.frame << s;
+ if (PROTO >= 5) [[likely]]
+ {
+ sc.offset[0] << s;
+ sc.offset[1] << s;
+ }
+ if (PROTO >= 6) [[likely]]
+ {
+ sc.bbox_size[0] << s;
+ sc.bbox_size[1] << s;
+ }
+ if (PROTO >= 7) [[likely]]
+ {
+ sc.bbox_offset[0] << s;
+ sc.bbox_offset[1] << s;
+ }
+ if (sc.active)
+ {
+ if (PROTO >= 4) [[likely]]
+ sc.delta << s;
+ else
+ sc.delta = (uint16_t)Math::clamp(int(s.read<float>() * 65535), 0, 65535);
+ }
+ }
+ global_coords coord{ch, local_coords{i}};
+ auto e = _world->make_object<scenery, false>(_world->make_id(), coord, sc);
+ (void)e;
+}
+
+void reader_state::deserialize_world(ArrayView<const char> buf, proto_t proto)
+{
+ fm_assert(_world != nullptr);
+ auto s = binary_reader{buf};
+ if (!(proto >= min_proto_version && proto <= proto_version))
+ fm_throw("bad proto version '{}' (should be between '{}' and '{}')"_cf,
+ (size_t)proto, (size_t)min_proto_version, (size_t)proto_version);
+ PROTO = proto;
+ fm_assert(PROTO > 0);
+ object_id object_counter = world::object_counter_init;
+ read_atlases(s);
+ if (PROTO >= 3) [[likely]]
+ read_sceneries(s);
+ if (PROTO >= 9) [[likely]]
+ read_strings(s);
+ if (PROTO >= 8) [[likely]]
+ object_counter << s;
+ read_chunks(s);
+ s.assert_end();
+ if (PROTO >= 8) [[likely]]
+ fm_assert(_world->object_counter() == world::object_counter_init);
+ if (PROTO >= 13) [[likely]]
+ _world->set_object_counter(object_counter);
+ else if (PROTO >= 8) [[likely]]
+ _world->set_object_counter(std::max(world::object_counter_init, object_counter));
+ preload_chunks();
+ _world = nullptr;
+}
+
+} // namespace
+
+namespace floormat {
+
+void world::deserialize_old(class world& w, ArrayView<const char> buf, proto_t proto)
+{
+ reader_state s{w};
+ s.deserialize_world(buf, proto);
+}
+
+} // namespace floormat