#include #include #include "luastack.hpp" #include "animqueue.hpp" #include "streambuffer.hpp" #include #include AnimStep::AnimStep() { clear(); } AnimStep::~AnimStep() {} void AnimStep::clear() { id_ = 0; bits_ = 0; action_ = ""; facing_ = 0; xyz_ = util::XYZ(0,0,0); graphic_ = ""; plane_ = ""; } void AnimStep::set_action(const std::string &act) { action_ = act; } void AnimStep::set_facing(float f) { bits_ |= HAS_FACING; facing_ = f; } void AnimStep::set_x(float f) { bits_ |= HAS_X; xyz_.x = f; } void AnimStep::set_y(float f) { bits_ |= HAS_Y; xyz_.y = f; } void AnimStep::set_z(float f) { bits_ |= HAS_Z; xyz_.z = f; } void AnimStep::set_xyz(const util::XYZ &xyz) { bits_ |= (HAS_X | HAS_Y | HAS_Z); xyz_ = xyz; } void AnimStep::set_graphic(const std::string &g) { bits_ |= HAS_GRAPHIC; graphic_ = g; } void AnimStep::set_plane(const std::string &p) { bits_ |= HAS_PLANE; plane_ = p; } bool AnimStep::exactly_equal(const AnimStep &other) const { if (id_ != other.id_) return false; if (bits_ != other.bits_) return false; if (action_ != other.action_) return false; if (facing_ != other.facing_) return false; if (xyz_ != other.xyz_) return false; if (graphic_ != other.graphic_) return false; if (plane_ != other.plane_) return false; return true; } bool AnimStep::logically_equal(const AnimStep &other) const { if (id_ != other.id_) return false; if (bits_ != other.bits_) return false; if (action_ != other.action_) return false; if (has_facing() && (facing_ != other.facing_)) return false; if (has_x() && (xyz_.x != other.xyz_.x)) return false; if (has_y() && (xyz_.y != other.xyz_.y)) return false; if (has_z() && (xyz_.z != other.xyz_.z)) return false; if (has_graphic() && (graphic_ != other.graphic_)) return false; if (has_plane() && (plane_ != other.plane_)) return false; return true; } bool AnimStep::state_equal(const AnimStep &other) const { if (facing_ != other.facing_) return false; if (xyz_ != other.xyz_) return false; if (graphic_ != other.graphic_) return false; if (plane_ != other.plane_) return false; return true; } void AnimStep::write_into(StreamBuffer *sb) const { sb->write_int64(id_); sb->write_int16(bits_); sb->write_string(action_); if (has_facing()) { sb->write_float(facing_); } if (has_x()) { sb->write_float(xyz_.x); } if (has_y()) { sb->write_float(xyz_.y); } if (has_z()) { sb->write_float(xyz_.z); } if (has_graphic()) { sb->write_string(graphic_); } if (has_plane()) { sb->write_string(plane_); } } void AnimStep::read_from(StreamBuffer *sb) { id_ = sb->read_int64(); bits_ = sb->read_int16(); action_ = sb->read_string(); if (has_facing()) { facing_ = sb->read_float(); } if (has_x()) { xyz_.x = sb->read_float(); } if (has_y()) { xyz_.y = sb->read_float(); } if (has_z()) { xyz_.z = sb->read_float(); } if (has_graphic()) { graphic_ = sb->read_string(); } if (has_plane()) { plane_ = sb->read_string(); } } void AnimStep::from_lua_store_string(lua_State *L, int idx, std::string *target, int16_t bits, const char *name) { if ((bits_ & bits)||(*target != "")) { luaL_error(L, "specified %s twice", name); } if (lua_type(L, idx) != LUA_TSTRING) { luaL_error(L, "Expected %s to be a string", name); } *target = lua_tostring(L, idx); bits_ |= bits; } void AnimStep::from_lua_store_number(lua_State *L, int idx, float *target, float offset, int16_t bits, const char *name) { if (bits_ & bits) { luaL_error(L, "specified %s twice", name); } if (lua_type(L, idx) != LUA_TNUMBER) { luaL_error(L, "Expected %s to be a number", name); } *target = lua_tonumber(L, idx) + offset; bits_ |= bits; } void AnimStep::from_lua(lua_State *L, int idx, const AnimStep &qback) { LuaSpecial tab(idx); LuaVar key, value; LuaStack LS(L, key, value); if (!LS.istable(tab)) { luaL_error(L, "animation spec must be a table"); } LS.set(key, LuaNil); while (LS.next(tab, key, value)) { if (!LS.isstring(key)) { luaL_error(L, "animation specs must be key/value where key is a string"); } std::string skey = LS.ckstring(key); if (skey == "action") { from_lua_store_string(L, value.index(), &action_, 0, "action"); } else if (skey == "graphic") { from_lua_store_string(L, value.index(), &graphic_, HAS_GRAPHIC, "graphic"); } else if (skey == "plane") { from_lua_store_string(L, value.index(), &plane_, HAS_PLANE, "plane"); } else if (skey == "x") { from_lua_store_number(L, value.index(), &xyz_.x, 0.0, HAS_X, "X coordinate"); } else if (skey == "y") { from_lua_store_number(L, value.index(), &xyz_.y, 0.0, HAS_Y, "Z coordinate"); } else if (skey == "z") { from_lua_store_number(L, value.index(), &xyz_.z, 0.0, HAS_Z, "Z coordinate"); } else if (skey == "dx") { from_lua_store_number(L, value.index(), &xyz_.x, qback.xyz().x, HAS_X, "X coordinate"); } else if (skey == "dy") { from_lua_store_number(L, value.index(), &xyz_.y, qback.xyz().y, HAS_Y, "Y coordinate"); } else if (skey == "dz") { from_lua_store_number(L, value.index(), &xyz_.z, qback.xyz().z, HAS_Z, "Z coordinate"); } else if (skey == "facing") { from_lua_store_number(L, value.index(), &facing_, 0.0, HAS_FACING, "facing"); } else { luaL_error(L, "Unrecognized animation spec: %s", skey.c_str()); } } } void AnimStep::keep_state_only() { bits_ = HAS_EVERYTHING; id_ = 0; action_ = ""; } void AnimStep::echo(const AnimStep &prev) { if (!has_facing()) { facing_ = prev.facing_; } if (!has_x()) { xyz_.x = prev.xyz_.x; } if (!has_y()) { xyz_.y = prev.xyz_.y; } if (!has_z()) { xyz_.z = prev.xyz_.z; } if (!has_graphic()) { graphic_ = prev.graphic_; } if (!has_plane()) { plane_ = prev.plane_; } } bool AnimStep::echoes(const AnimStep &prev) const { if (!has_facing() && (facing_ != prev.facing_)) { return false; } if (!has_x() && (xyz_.x != prev.xyz_.x)) { return false; } if (!has_y() && (xyz_.y != prev.xyz_.y)) { return false; } if (!has_z() && (xyz_.z != prev.xyz_.z)) { return false; } if (!has_graphic() && (graphic_ != prev.graphic_)) { return false; } if (!has_plane() && (plane_ != prev.plane_)) { return false; } return true; } std::string AnimStep::debug_string() const { std::ostringstream oss; oss << "id=" << id(); oss << " action=" << action(); if (has_plane()) { oss << " plane=" << plane(); } if (has_x()) { oss << " x=" << xyz().x; } if (has_y()) { oss << " y=" << xyz().y; } if (has_z()) { oss << " z=" << xyz().z; } if (has_facing()) { oss << " facing=" << facing(); } if (has_graphic()) { oss << " graphic=" << graphic(); } return oss.str(); } bool AnimStep::from_string(const std::string &config) { clear(); util::StringVec parts = util::split(config, ' '); for (int i = 0; i < int(parts.size()); i++) { const std::string &part = parts[i]; if (part == "") continue; util::StringVec lr = util::split(part, '='); if (lr.size() != 2) return false; const std::string &key = lr[0]; const std::string &val = lr[1]; if (key == "action") { action_ = val; } else if (key == "id") { int64_t id = util::strtoint(val, -1); if (id < 0) return false; id_ = id; } else if (key == "plane") { set_plane(val); } else if (key == "x") { double v = util::strtodouble(val); if (std::isnan(v)) return false; set_x(v); } else if (key == "y") { double v = util::strtodouble(val); if (std::isnan(v)) return false; set_y(v); } else if (key == "z") { double v = util::strtodouble(val); if (std::isnan(v)) return false; set_z(v); } else if (key == "facing") { double v = util::strtodouble(val); if (std::isnan(v)) return false; set_facing(v); } else if (key == "graphic") { set_graphic(val); } else { return false; } } return true; } AnimQueue::AnimQueue(util::WorldType wt) { version_autoinc_ = (wt == util::WORLD_TYPE_MASTER); size_limit_ = 10; // Default size limit. steps_.emplace_back(); steps_.front().keep_state_only(); version_number_ = version_autoinc_ ? 1 : 0; } void AnimQueue::mutated() { if (version_autoinc_) { version_number_ += 1; } else { version_number_ = 0; } } bool AnimQueue::size_and_steps_equal(const AnimQueue &other) const { if (size_limit_ != other.size_limit_) { return false; } if (steps_.size() != other.steps_.size()) { return false; } for (int i = 0; i < int(steps_.size()); i++) { if (!steps_[i].exactly_equal(other.steps_[i])) { return false; } } return true; } void AnimQueue::full_clear_and_set_limit(int n) { assert(n >= 1); steps_.clear(); steps_.emplace_back(); steps_.front().keep_state_only(); size_limit_ = n; version_number_ = version_autoinc_ ? 1 : 0; } void AnimQueue::clear(const std::string &plane) { steps_.clear(); steps_.emplace_back(); steps_.front().set_plane(plane); steps_.front().keep_state_only(); mutated(); } void AnimQueue::set_limit(int n) { assert(n >= 1); size_limit_ = n; if (int(steps_.size()) > n) { while (int(steps_.size()) > n) { steps_.pop_front(); } steps_.front().keep_state_only(); } mutated(); } void AnimQueue::add(int64_t id, const AnimStep &step) { AnimStep copy = step; copy.echo(steps_.back()); copy.id_ = id; steps_.push_back(copy); while (int(steps_.size()) > size_limit_) { steps_.pop_front(); } steps_.front().keep_state_only(); mutated(); } bool AnimQueue::valid() const { // Size limit must be between 2 and 250 if ((size_limit_ < 1) || (size_limit_ > 250)) { return false; } // Animqueue must have at least one step, and no more than 250. if (steps_.empty()) { return false; } if (steps_.size() > 250) { return false; } // First action should be blank. if (steps_[0].action_ != "") { return false; } // First step should have all bits. if (steps_[0].bits_ != AnimStep::HAS_EVERYTHING) { return false; } // Any unset bit should correspond to a value copied from the previous step. for (size_t i = 1; i < steps_.size(); i++) { const AnimStep &prev = steps_[i - 1]; const AnimStep &curr = steps_[i]; if (!curr.echoes(prev)) return false; } return true; } std::string AnimQueue::steps_debug_string() const { std::ostringstream oss; for (int i = 0; i < int(size()); i++) { oss << nth(i).debug_string() << "; "; } return oss.str(); } std::string AnimQueue::full_debug_string() const { std::ostringstream oss; for (int i = 0; i < int(size()); i++) { oss << nth(i).debug_string() << "; "; } oss << "version=" << version_number(); oss << "; limit=" << size_limit(); return oss.str(); } void AnimQueue::serialize(StreamBuffer *sb) const { assert(valid()); // can't serialize an invalid animqueue. sb->write_uint8(size_limit_); sb->write_uint8(steps_.size()); for (const AnimStep &step : steps_) { step.write_into(sb); } } void AnimQueue::deserialize(StreamBuffer *sb) { size_limit_ = sb->read_uint8(); size_t nsteps = sb->read_uint8(); steps_.resize(nsteps); for (size_t i = 0; i < nsteps; i++) { AnimStep &step = steps_[i]; step.read_from(sb); if (i > 0) step.echo(steps_[i - 1]); } version_number_ = 0; } bool AnimQueue::version_identical(const AnimQueue &auth) const { return version_number_ == auth.version_number_; } bool AnimQueue::diff(const AnimQueue &auth, StreamBuffer *sb) const { assert(valid()); assert(auth.valid()); // Fast path to equivalence detection if (version_identical(auth)) { assert(size_and_steps_equal(auth)); sb->write_uint8(255); return false; } // Another path to equivalence detection if (size_and_steps_equal(auth)) { sb->write_uint8(255); return false; } // Write the first element. assert(auth.steps_.size() < 255); sb->write_uint8(auth.steps_.size()); sb->write_uint8(auth.size_limit_); const AnimStep &first = auth.steps_[0]; int match = 0; while ((match < int(steps_.size())) && (!steps_[match].state_equal(first))) { match++; } if (match == int(steps_.size())) { sb->write_uint8(255); first.write_into(sb); } else { sb->write_uint8(match); } // Index the remaining elements by id. std::map index; for (int i = 1; i < int(steps_.size()); i++) { index[steps_[i].id_] = i; } // Write the remaining elements. for (int i = 1; i < int(auth.steps_.size()); i++) { const AnimStep &step = auth.steps_[i]; auto iter = index.find(step.id()); if (iter == index.end()) { sb->write_uint8(255); step.write_into(sb); } else { const AnimStep &local = steps_[iter->second]; if (local.exactly_equal(step)) { sb->write_uint8(iter->second); } else { sb->write_uint8(255); step.write_into(sb); } } } return true; } void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) { int len = sb->read_uint8(); if (len == 255) { return; } DebugLine(dbc) << "AnimQueue modified"; size_limit_ = sb->read_uint8(); // Decode the diff, stop at eof. std::deque old = std::move(steps_); steps_.clear(); for (int i = 0; i < len; i++) { uint8_t index = sb->read_uint8(); if (index < 255) { assert(index < old.size()); steps_.push_back(old[index]); } else { AnimStep step; step.read_from(sb); steps_.push_back(step); } int size = steps_.size(); if (size > 1) { steps_[size-1].echo(steps_[size-2]); } else { steps_[0].keep_state_only(); } } mutated(); } void AnimQueue::update_version(const AnimQueue &auth) { assert(size_and_steps_equal(auth)); version_number_ = auth.version_number_; } const AnimStep &AnimQueue::back() const { return steps_.back(); } static bool diff_works(const AnimQueue &master, AnimQueue &sync) { StreamBuffer sb; sync.diff(master, &sb); sync.patch(&sb, nullptr); return sync.size_and_steps_equal(master); } LuaDefine(unittests_animqueue, "c") { // Useful objects. AnimStep stp; AnimQueue aq(util::WORLD_TYPE_MASTER); AnimQueue aqds(util::WORLD_TYPE_S_SYNC); StreamBuffer sb; // Debug string of a newly initialized queue LuaAssert(L, aq.valid()); LuaAssertStrEq(L, aq.full_debug_string(), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; version=1; limit=10"); // Test the step setters. stp.clear(); LuaAssertStrEq(L, stp.debug_string(), "id=0 action="); stp.set_facing(180); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= facing=180"); stp.set_x(3); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= x=3 facing=180"); stp.set_y(4); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= x=3 y=4 facing=180"); stp.set_z(5); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= x=3 y=4 z=5 facing=180"); stp.set_plane("somewhere"); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= plane=somewhere x=3 y=4 z=5 facing=180"); stp.set_graphic("something"); LuaAssertStrEq(L, stp.debug_string(), "id=0 action= plane=somewhere x=3 y=4 z=5 facing=180 graphic=something"); // Test the step debug string parser. LuaAssert(L, stp.from_string("id=123 action=walk x=1 y=2 z=3 facing=4 plane=p graphic=g")); LuaAssertStrEq(L, stp.debug_string(), "id=123 action=walk plane=p x=1 y=2 z=3 facing=4 graphic=g"); // Test that we can clear a queue. aq.full_clear_and_set_limit(3); LuaAssert(L, aq.valid()); LuaAssertStrEq(L, aq.full_debug_string(), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; version=1; limit=3"); // Add a step to a queue. aq.full_clear_and_set_limit(3); LuaAssert(L, stp.from_string("action=walk")); aq.add(12345, stp); LuaAssertStrEq(L, aq.full_debug_string(), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=12345 action=walk; " "version=2; limit=3"); // Exceed the length limit, dropping first element. aq.full_clear_and_set_limit(3); LuaAssert(L, stp.from_string("action=walk plane=foo")); aq.add(12345, stp); aq.add(12346, stp); aq.add(12347, stp); LuaAssertStrEq(L, aq.full_debug_string(), "id=0 action= plane=foo x=0 y=0 z=0 facing=0 graphic=; " "id=12346 action=walk plane=foo; " "id=12347 action=walk plane=foo; " "version=4; limit=3" ); // Test serialization and deserialization. aq.full_clear_and_set_limit(5); LuaAssert(L, stp.from_string("action=walk x=3 y=4 z=5")); aq.add(12345, stp); LuaAssert(L, stp.from_string("action=setgraphic graphic=banana")); aq.add(12346, stp); LuaAssert(L, stp.from_string("action=setfacing facing=301")); aq.add(12347, stp); aq.serialize(&sb); aqds.deserialize(&sb); LuaAssertStrEq(L, aqds.full_debug_string(), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=12345 action=walk x=3 y=4 z=5; " "id=12346 action=setgraphic graphic=banana; " "id=12347 action=setfacing facing=301; " "version=0; limit=5" ); // Test difference transmission aq.full_clear_and_set_limit(10); aqds.full_clear_and_set_limit(10); // Add a single action to the queue and DT LuaAssert(L, stp.from_string("action=walk x=3 y=4 z=5")); aq.add(12345, stp); LuaAssert(L, diff_works(aq, aqds)); // Add another action and DT LuaAssert(L, stp.from_string("action=fnord plane=where facing=123")); aq.add(232, stp); LuaAssert(L, diff_works(aq, aqds)); // Change the queue size limit. aq.set_limit(13); LuaAssert(L, diff_works(aq, aqds)); // compare again, should be no differences. LuaAssert(L, aqds.version_identical(aq)); LuaAssert(L, aqds.size_and_steps_equal(aq)); LuaAssert(L, diff_works(aq, aqds)); // Discard all but the last action. aq.set_limit(1); LuaAssert(L, diff_works(aq, aqds)); return 0; }