#define _USE_MATH_DEFINES #include "wrap-sstream.hpp" #include "wrap-map.hpp" #include "util.hpp" #include "animqueue.hpp" #include "luastack.hpp" #include "streambuffer.hpp" #include #include #include util::SharedStdString AnimQueue::blankqueue_; void AnimQueue::initialize_module() { AnimQueue queue; blankqueue_ = queue.get_encoded_queue(); } static int64_t hash_encstep(int64_t prev, std::string_view s) { // We drop the most significant bit to ensure that the value // can be represented as a nonnegative int64. return int64_t(0x7FFFFFFF & util::hash_string(util::HashValue(123, prev), s).first); } // Parse a value. This is meant for unit testing only. The // parser isn't powerful enough to express all possible values. static void parse_value(std::string_view vstr, AnimValue *v) { // Try to interpret vstr as a boolean. bool is_true = (vstr == "true"); bool is_false = (vstr == "false"); if (is_true || is_false) { v->set_boolean(is_true); return; } // Try to interpret vstr as a number. if (sv::valid_number(vstr, true, true, true, false)) { v->set_number(std::atof(std::string(vstr).c_str())); return; } // Try to interpret vstr as a vector. eng::vector parts = util::split(eng::string(vstr), ','); if ((parts.size() == 3) && (sv::valid_number(parts[0], true, true, true, false)) && (sv::valid_number(parts[1], true, true, true, false)) && (sv::valid_number(parts[2], true, true, true, false))) { double x = std::atof(parts[0].c_str()); double y = std::atof(parts[1].c_str()); double z = std::atof(parts[2].c_str()); v->set_dxyz(util::DXYZ(x,y,z)); return; } // If it doesn't parse as any of the above, it's a string. v->set_string(vstr); } static AnimValue parse_anim_value(LuaCoreStack &LS, LuaSlot val) { AnimValue result; int type = LS.type(val); switch (type) { case LUA_TBOOLEAN: { auto tbool = LS.tryboolean(val); if (tbool) result.set_boolean(*tbool); } case LUA_TNUMBER: { auto tnum = LS.trynumber(val); if (tnum) result.set_number(*tnum); } case LUA_TSTRING: { auto tstr = LS.trystringview(val); if (tstr) result.set_string(*tstr); } case LUA_TTABLE: { auto txyz = LS.tryxyz(val); if (txyz) result.set_dxyz(*txyz); } case LUA_TLIGHTUSERDATA: { auto ttoken = LS.trytoken(val); if (ttoken) result.set_token(*ttoken); } } return result; } void AnimState::set_persistent(const eng::string &name) { map_[name].persistent = true; } void AnimState::print_debug_string(eng::ostringstream &oss) { bool first = true; if (map_.empty()) { oss << "[empty]"; } for (const auto &pair : map_) { if (!first) oss << " "; const eng::string &name = pair.first; const AnimValue &value = pair.second; oss << name; if (value.persistent) { oss << "="; } else { oss << ":"; } switch (value.type) { case SimpleDynamicTag::UNINITIALIZED: oss << "UNINITIALIZED"; break; case SimpleDynamicTag::STRING: oss << value.s; break; case SimpleDynamicTag::TOKEN: oss << "[" << value.s << "]"; break; case SimpleDynamicTag::NUMBER: oss << value.x; break; case SimpleDynamicTag::BOOLEAN: oss << ((value.x == 1.0) ? "true":"false"); break; case SimpleDynamicTag::VECTOR: oss << value.x << "," << value.y << "," << value.z; break; default: assert(false); } first = false; } } eng::string AnimState::debug_string() { eng::ostringstream oss; print_debug_string(oss); return oss.str(); } eng::string AnimState::encode() const { StreamBuffer sb; for (const auto &pair : map_) { const eng::string &name = pair.first; const AnimValue &value = pair.second; sb.write_string(name); sb.write_bool(value.persistent); sb.write_simple_dynamic(value); } return eng::string(sb.view()); } void AnimState::decode(std::string_view s) { map_.clear(); StreamBuffer sb(s); while (!sb.empty()) { eng::string name = sb.read_string(); AnimValue &value = map_[name]; value.persistent = sb.read_bool(); sb.read_simple_dynamic(&value); } } void AnimState::decode_persistent(std::string_view s) { map_.clear(); StreamBuffer sb(s); AnimValue dummy; while (!sb.empty()) { eng::string name = sb.read_string(); bool persistent = sb.read_bool(); if (persistent) { AnimValue &value = map_[name]; value.persistent = persistent; sb.read_simple_dynamic(&value); } else { sb.read_simple_dynamic(&dummy); } } } eng::string AnimState::add_default(const eng::string &name, const AnimValue &def, const AnimState *other) { AnimValue &value = map_[name]; value.persistent = true; if (value.type == SimpleDynamicTag::UNINITIALIZED) { if (other != nullptr) { auto otheriter = other->map_.find(name); if (otheriter != other->map_.end()) { if (otheriter->second.persistent && otheriter->second.type == def.type) { value.copy_value(otheriter->second); return ""; } } } value.copy_value(def); return ""; } if (value.type != def.type) { return util::ss("Animation key ", name, " must be a ", def.type_name()); } return ""; } eng::string AnimState::add_defaults(const AnimState *other) { eng::string err; AnimValue defval; defval.set_dxyz(util::DXYZ(0,0,0)); err = add_default("xyz", defval, other); if (!err.empty()) return err; defval.set_string("nowhere"); err = add_default("plane", defval, other); if (!err.empty()) return err; defval.set_number(0.0); err = add_default("facing", defval, other); if (!err.empty()) return err; defval.set_string("unknown"); err = add_default("bp", defval, other); if (!err.empty()) return err; return ""; } eng::string AnimState::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto) { LuaVar key, val; LuaExtStack LS(LS0.state(), key, val); util::DXYZ xyz; clear(); if (!LS.istable(tab)) { return "A lua animstate must be a table."; } LS.set(key, LuaNil); while (LS.next(tab, key, val)) { if (!LS.isstring(key)) { return "in animation key-value pairs, key must be a string."; } eng::string name = LS.ckstring(key); if (!sv::is_lua_id(name)) { return "in animation key-value pairs, key must be a valid lua identifier."; } AnimValue parsedvalue = parse_anim_value(LS, val); if (parsedvalue.type == SimpleDynamicTag::UNINITIALIZED) { return "in animation key-value pairs, value must be string, token, number, boolean, or xyz"; } if (parsedvalue.is_token("auto") && !allowauto) { return "in animation key-value pairs, value must not be [auto] here."; } AnimValue &mapentry = map_[name]; mapentry.copy_value(parsedvalue); mapentry.persistent = persistent; } return ""; } eng::string AnimState::merge(const AnimState &previous, const AnimState &update) { // Copy everything over from the previous entry. map_ = previous.map_; for (const auto &pair : update.map_) { const eng::string &name = pair.first; AnimValue &dst = map_[name]; const AnimValue &src = pair.second; // Handle autocalculation rules. if (src.is_token("auto")) { if (name == "facing") { if (!dst.persistent || dst.type != SimpleDynamicTag::NUMBER) { return "Cannot auto-calculate facing because facing has not been specified as a persistent number"; } const auto xyz_previous = previous.map_.find("xyz"); const auto xyz_update = update.map_.find("xyz"); if ((xyz_previous == previous.map_.end()) || (xyz_update == update.map_.end()) || (xyz_previous->second.type != SimpleDynamicTag::VECTOR) || (xyz_update->second.type != SimpleDynamicTag::VECTOR)) { return "Cannot auto-calculate facing because before/after xyz coordinates are not present"; } double dx = xyz_update->second.x - xyz_previous->second.x; double dy = xyz_update->second.y - xyz_previous->second.y; // If dx and dy are both zero, leave the facing unmodified. if ((dx != 0.0) || (dy != 0.0)) { double facing = atan2(dy, dx) * 180.0 / M_PI; dst.set_number(facing); } } else { return util::ss("No rule to automatically calculate ", name); } continue; } if (dst.persistent && (src.type != dst.type)) { return util::ss("Wrong data type for ", name, ", should be ", dst.type_name()); } dst.copy_value(src); } return ""; } void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool transient, bool persistent) { LuaVar name, val; LuaExtStack LS(LS0.state(), name, val); LS.newtable(tab); for (const auto &pair : map_) { if (pair.second.persistent) { if (!persistent) continue; } else { if (!transient) continue; } LS.set(name, pair.first); const AnimValue &value = pair.second; if (value.type == SimpleDynamicTag::BOOLEAN) { LS.set(val, (value.x == 1.0)); } else if (value.type == SimpleDynamicTag::NUMBER) { LS.set(val, value.x); } else if (value.type == SimpleDynamicTag::STRING) { LS.set(val, std::string_view(value.s)); } else if (value.type == SimpleDynamicTag::TOKEN) { LS.set(val, LuaToken(value.s)); } else if (value.type == SimpleDynamicTag::VECTOR) { LS.newtable(val); LS.rawset(val, 1, value.x); LS.rawset(val, 2, value.y); LS.rawset(val, 3, value.z); } LS.rawset(tab, name, val); } } // The syntax used by this parser is not general enough to represent all // possible strings. That's OK, though, since it's just for unit testing. void AnimState::parse(std::string_view config) { while (true) { config = sv::ltrim(config); if (config.empty()) break; eng::string name(sv::read_simple_identifier(config)); assert(!name.empty()); AnimValue &value = map_[name]; bool has_equal = sv::has_prefix(config, "="); bool has_colon = sv::has_prefix(config, ":"); assert(has_equal || has_colon); config.remove_prefix(1); value.persistent = has_equal; eng::string vstr(sv::read_to_space(config)); parse_value(vstr, &value); } } void AnimState::clear_and_parse(std::string_view config) { map_.clear(); parse(config); } void AnimCoreState::decode(std::string_view s) { plane.clear(); xyz = 0.0; StreamBuffer sb(s); AnimValue value; while (!sb.empty()) { eng::string name = sb.read_string(); bool persistent = sb.read_bool(); sb.read_simple_dynamic(&value); if (persistent) { if ((name == "xyz") && (value.type == SimpleDynamicTag::VECTOR)) xyz = util::DXYZ(value.x, value.y, value.z); if ((name == "plane") && (value.type == SimpleDynamicTag::STRING)) plane = value.s; } } } int AnimQueue::get_size_limit() const { if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); return sb.read_uint8(); } int AnimQueue::get_actual_size() const { if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); sb.read_bytes(1); return sb.read_uint8(); } int64_t AnimQueue::get_final_hash() const { if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); sb.read_bytes(2); return sb.read_uint64(); } std::string_view AnimQueue::get_final_encstep() const { if (encqueue_ == nullptr) return std::string_view(); StreamBuffer sb(*encqueue_); sb.read_bytes(10); return sb.read_string_view(); } AnimQueue::QueueRange AnimQueue::get_range(int lo, int hi) { // Clamp lo and hi to the valid range (0 to actual_size). // int actual_size = get_actual_size(); if (lo < 0) lo = 0; if (hi > actual_size) hi = actual_size; // Abort early if the range is empty. This avoids several edge cases. // if (lo >= hi) return QueueRange(0, std::string_view()); // Get the entries. // std::string_view queueview(*encqueue_); StreamBuffer sb(queueview); sb.read_bytes(2); // Skip over the header. for (int i = 0; i < lo; i++) { sb.read_uint64(); sb.read_string_view(); } int pos1 = sb.total_reads(); for (int i = lo; i < hi; i++) { sb.read_uint64(); sb.read_string_view(); } int pos2 = sb.total_reads(); return QueueRange(hi-lo, queueview.substr(pos1, pos2 - pos1)); } int64_t AnimQueue::hash_encstep(const QueueRange &prev, std::string_view s) { int64_t prev_hash = 0; if (prev.size > 0) { StreamBuffer retsb(prev.entries); prev_hash = retsb.read_int64(); } return ::hash_encstep(prev_hash, s); } void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi) { // Get the retained entries. QueueRange keeprange = get_range(keeplo, keephi); // Encode everything into a binary blob. StreamBuffer result; result.write_uint8(limit); result.write_uint8(keeprange.size + (add ? 1:0)); if (add) { int64_t add_hash = hash_encstep(keeprange, add_enc); result.write_uint64(add_hash); result.write_string(add_enc); } result.write_bytes(keeprange.entries); // Replace the shared string. encqueue_ = std::make_shared(result.view()); } AnimQueue::AnimQueue() { update_encqueue(10, true, AnimState().encode(), 0, 0); } void AnimQueue::clear(const AnimState &state) { update_encqueue(get_size_limit(), true, state.encode(), 0, 0); } void AnimQueue::clear() { update_encqueue(get_size_limit(), true, AnimState().encode(), 0, 0); } void AnimQueue::set_limit(int limit) { assert((limit >= 2) && (limit <= 250)); update_encqueue(limit, false, std::string_view(), 0, limit); } void AnimQueue::add(const AnimState &state) { int limit = get_size_limit(); update_encqueue(limit, true, state.encode(), 0, limit - 1); } void AnimQueue::replace(const AnimState &state) { int limit = get_size_limit(); update_encqueue(limit, true, state.encode(), 1, limit); } void AnimQueue::serialize(StreamBuffer *sb) const { sb->write_string(*encqueue_); } void AnimQueue::deserialize(StreamBuffer *sb) { encqueue_ = std::make_shared(sb->read_string_view()); } bool AnimQueue::diff(const AnimQueue &auth, StreamBuffer *sb) const { // Fast check for exactly equivalent. If equivalent, skip all the work. if (exactly_equal_fast(auth)) { assert(exactly_equal(auth)); sb->write_bool(false); return false; } // TODO: maybe send less data? sb->write_bool(true); sb->write_string(*auth.encqueue_); return true; } void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) { bool changed = sb->read_bool(); if (!changed) { return; } DebugLine(dbc) << "AnimQueue modified"; encqueue_ = std::make_shared(sb->read_string_view()); } bool AnimQueue::exactly_equal(const AnimQueue &other) const { if (*encqueue_ != *other.encqueue_) return false; return true; } bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const { if (encqueue_->size() != other.encqueue_->size()) return false; if (encqueue_->compare(0, 10, *other.encqueue_) != 0) return false; return true; } void AnimQueue::print_debug_string(eng::ostringstream &oss, bool full) const { bool first = true; // Break out the steps. eng::vector encsteps; StreamBuffer sb(*encqueue_); int size_limit = sb.read_uint8(); int actual_size = sb.read_uint8(); if (full) { oss << "limit=" << size_limit; first = false; } for (int i = 0; i < actual_size; i++) { sb.read_uint64(); encsteps.push_back(sb.read_string_view()); } assert(sb.empty()); for (int i = encsteps.size() - 1; i >= 0; i --) { if (!first) oss << "; "; AnimState state(encsteps[i]); state.print_debug_string(oss); first = false; } } eng::string AnimQueue::steps_debug_string() const { eng::ostringstream oss; print_debug_string(oss, false); return oss.str(); } eng::string AnimQueue::full_debug_string() const { eng::ostringstream oss; print_debug_string(oss, true); return oss.str(); } AnimCoreState AnimQueue::get_final_core_state() const { std::string_view encstep = get_final_encstep(); AnimCoreState result; result.decode(encstep); return result; } AnimState AnimQueue::get_final_persistent() const { std::string_view encstep = get_final_encstep(); AnimState result; result.decode_persistent(encstep); return result; } AnimState AnimQueue::get_final_everything() const { std::string_view encstep = get_final_encstep(); AnimState result; result.decode(encstep); return result; } LuaDefine(unittests_animqueue, "", "some unit tests") { // Useful objects. AnimQueue aq, aqs; StreamBuffer sb; AnimState astate; eng::string enc; AnimCoreState core; // Debug string of a newly initialized queue LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]"); // Test AnimState simple setters. astate.set_string("color", "blue"); astate.set_dxyz("xyz", util::DXYZ(1,2,3)); astate.set_number("half", 0.5); astate.set_boolean("nice", true); LuaAssertStrEq(L, astate.debug_string(), "color:blue half:0.5 nice:true xyz:1,2,3"); // // Test AnimState simple getters. // LuaAssert(L, astate.get_string("color") == "blue"); // LuaAssert(L, astate.get_xyz("xyz") == util::DXYZ(1,2,3)); // LuaAssert(L, astate.get_number("half") == 0.5); // LuaAssert(L, astate.get_boolean("nice") == true); // // Test AnimState simple getters on nonexistent data. // LuaAssert(L, astate.get_string("q") == ""); // LuaAssert(L, astate.get_xyz("q") == util::DXYZ(0,0,0)); // LuaAssert(L, astate.get_number("q") == 0.0); // LuaAssert(L, astate.get_boolean("q") == false); // // Test AnimState simple getters on wrong-type data. // LuaAssert(L, astate.get_string("half") == ""); // LuaAssert(L, astate.get_xyz("half") == util::DXYZ(0,0,0)); // LuaAssert(L, astate.get_number("color") == 0.0); // LuaAssert(L, astate.get_boolean("color") == false); // Test AnimState persistence manipulation. astate.set_persistent("color"); astate.set_persistent("nice"); LuaAssertStrEq(L, astate.debug_string(), "color=blue half:0.5 nice=true xyz:1,2,3"); // Test AnimState parser. astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false"); LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5"); // Test animstate encoding and decoding. astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false"); enc = astate.encode(); astate.clear(); astate.decode(enc); LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5"); astate.decode_persistent(enc); LuaAssertStrEq(L, astate.debug_string(), "mean=true pos=3,4,5"); // Test AnimCoreState.decode // astate.clear_and_parse("color=blue xyz=1,2,3 plane=banana chicken=3"); core.decode(astate.encode()); LuaAssert(L, core.plane == "banana"); LuaAssert(L, core.xyz == util::DXYZ(1,2,3)); // Verify that a newly-constructed AnimQueue is in a reasonable default state. // LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]"); // Clear an AnimQueue to a specified initial state. // astate.clear_and_parse("color=blue xyz=1,2,3 plane=somewhere"); aq.clear(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; color=blue plane=somewhere xyz=1,2,3"); // Add animation steps to animation queue. // Note: each step is independent of the previous one, no composition is being done. // astate.clear_and_parse("xyz=1,2,3 plane=earth"); aq.clear(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3"); astate.clear_and_parse("xyz=4,5,6 action:jump"); aq.add(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6"); astate.clear_and_parse("plane=moon airline:southwest"); aq.add(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon"); astate.clear_and_parse("color=blue"); aq.add(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); // Try reducing the animation queue size limit. // aq.set_limit(2); LuaAssertStrEq(L, aq.full_debug_string(), "limit=2; airline:southwest plane=moon; color=blue"); // Test get_final_persistent, get_final_everything, get_final_core_state // astate.clear_and_parse("action:jump plane=earth xyz=1,2,3 bouncy:true"); aq.clear(astate); astate = aq.get_final_persistent(); LuaAssertStrEq(L, astate.debug_string(), "plane=earth xyz=1,2,3"); astate = aq.get_final_everything(); LuaAssertStrEq(L, astate.debug_string(), "action:jump bouncy:true plane=earth xyz=1,2,3"); core = aq.get_final_core_state(); LuaAssert(L, core.plane == "earth"); LuaAssert(L, core.xyz == util::DXYZ(1,2,3)); // Serialize a queue. // aq.set_limit(10); astate.clear_and_parse("xyz=1,2,3 plane=earth"); aq.clear(astate); astate.clear_and_parse("xyz=4,5,6 action:jump"); aq.add(astate); astate.clear_and_parse("plane=moon airline:southwest"); aq.add(astate); astate.clear_and_parse("color=blue"); aq.add(astate); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); aq.serialize(&sb); // Deserialize a queue. // aqs.set_limit(7); aqs.clear(); LuaAssert(L, !aqs.exactly_equal(aq)); aqs.deserialize(&sb); LuaAssert(L, aqs.exactly_equal(aq)); LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); // Test diff and patch. // LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); aqs.set_limit(7); aqs.clear(); sb.clear(); aqs.diff(aq, &sb); //int difflen1 = sb.fill(); LuaAssert(L, !aqs.exactly_equal(aq)); aqs.patch(&sb, nullptr); LuaAssert(L, aqs.exactly_equal(aq)); LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); // Test that diff and patch are more efficient when the two queues contain some shared steps. // LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); astate.clear_and_parse("xyz=4,5,6 action:jump"); aqs.clear(astate); astate.clear_and_parse("plane=earth xyz=1,2,3"); aqs.add(astate); astate.clear_and_parse("plane=moon airline:southwest"); aqs.add(astate); LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; action:jump xyz=4,5,6; plane=earth xyz=1,2,3; airline:southwest plane=moon"); sb.clear(); aqs.diff(aq, &sb); //int difflen2 = sb.fill(); LuaAssert(L, !aqs.exactly_equal(aq)); aqs.patch(&sb, nullptr); LuaAssert(L, aqs.exactly_equal(aq)); LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue"); // TODO: if we make the diff routine more efficient, this should be true. // LuaAssert(L, difflen2 < (difflen1 / 2)); return 0; }