#include "wrap-sstream.hpp" #include "wrap-map.hpp" #include "util.hpp" #include "animqueue.hpp" #include "luastack.hpp" #include "streambuffer.hpp" #include #include #include static const char *vtname(AnimValueType vt) { switch (vt) { case T_UNINITIALIZED: return "uninitialized"; case T_BOOLEAN: return "boolean"; case T_NUMBER: return "number"; case T_STRING: return "string"; case T_XYZ: return "xyz"; default: return "unknown"; } } uint64_t hash_encstep(uint64_t prev, std::string_view s) { return util::hash_string(util::HashValue(123, prev), s).first; } void AnimValue::set_boolean(bool b) { type = T_BOOLEAN; str.clear(); xyz = (b ? 1.0:0.0); } void AnimValue::set_number(double n) { type = T_NUMBER; str.clear(); xyz = util::DXYZ(n, 0, 0); } void AnimValue::set_xyz(const util::DXYZ &v) { type = T_XYZ; str.clear(); xyz = v; } void AnimValue::set_string(std::string_view sv) { type = T_STRING; str = sv; xyz = 0.0; } bool AnimValue::get_boolean() const { if (type != T_BOOLEAN) return false; return (xyz.x == 1.0); } double AnimValue::get_number() const { if (type != T_NUMBER) return 0.0; return xyz.x; } const util::DXYZ &AnimValue::get_xyz() const { static util::DXYZ zero; if (type != T_XYZ) return zero; return xyz; } std::string_view AnimValue::get_string() const { if (type != T_STRING) return std::string_view(""); return std::string_view(str); } void AnimValue::copy_value(const AnimValue &other) { type = other.type; str = other.str; xyz = other.xyz; } static void encode_value(StreamBuffer *sb, const AnimValue *v) { sb->write_uint8(uint8_t(v->type)); switch(v->type) { case T_NUMBER: sb->write_double(v->xyz.x); break; case T_BOOLEAN: sb->write_bool(v->xyz.x == 1.0); break; case T_XYZ: sb->write_dxyz(v->xyz); break; case T_STRING: sb->write_string(v->str); break; default: assert(false); } } static void decode_value(StreamBuffer *sb, AnimValue *v) { AnimValueType type = AnimValueType(sb->read_uint8()); switch (type) { case T_NUMBER: v->set_number(sb->read_double()); break; case T_BOOLEAN: v->set_boolean(sb->read_bool()); break; case T_XYZ: v->set_xyz(sb->read_dxyz()); break; case T_STRING: v->set_string(sb->read_string()); break; default: assert(false); } } // 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_xyz(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, LuaSlot tmp) { AnimValue result; if (LS.isboolean(val)) { result.set_boolean(LS.ckboolean(val)); } else if (LS.isnumber(val)) { result.set_number(LS.cknumber(val)); } else if (LS.isstring(val)) { result.set_string(LS.ckstring(val)); } else if (LS.istable(val)) { util::DXYZ xyz; LS.rawget(tmp, val, 1); if (!LS.isnumber(tmp)) return result; xyz.x = LS.cknumber(tmp); LS.rawget(tmp, val, 2); if (!LS.isnumber(tmp)) return result; xyz.y = LS.cknumber(tmp); LS.rawget(tmp, val, 3); if (!LS.isnumber(tmp)) return result; xyz.z = LS.cknumber(tmp); result.set_xyz(xyz); } return result; } void AnimState::set_persistent(const eng::string &name) { map_[name].persistent = true; } bool AnimState::get_boolean(const eng::string &name) { auto iter = map_.find(name); if (iter == map_.end()) return false; return iter->second.get_boolean(); } double AnimState::get_number(const eng::string &name) { auto iter = map_.find(name); if (iter == map_.end()) return 0.0; return iter->second.get_number(); } util::DXYZ AnimState::get_xyz(const eng::string &name) { static util::DXYZ zero; auto iter = map_.find(name); if (iter == map_.end()) return zero; return iter->second.get_xyz(); } std::string_view AnimState::get_string(const eng::string &name) { auto iter = map_.find(name); if (iter == map_.end()) return std::string_view(""); return iter->second.get_string(); } void AnimState::set_boolean(const eng::string &name, bool v) { AnimValue &value = map_[name]; value.set_boolean(v); } void AnimState::set_number(const eng::string &name, double v) { AnimValue &value = map_[name]; value.set_number(v); } void AnimState::set_xyz(const eng::string &name, const util::DXYZ &v) { AnimValue &value = map_[name]; value.set_xyz(v); } void AnimState::set_string(const eng::string &name, std::string_view v) { AnimValue &value = map_[name]; value.set_string(v); } 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 T_NUMBER: oss << value.xyz.x; break; case T_BOOLEAN: oss << ((value.xyz.x == 1.0) ? "true":"false"); break; case T_XYZ: oss << value.xyz; break; case T_STRING: oss << value.str; 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); encode_value(&sb, &value); } return sb.read_entire_contents(); } void AnimState::decode(std::string_view s) { map_.clear(); StreamBuffer sb(s); while (!sb.empty()) { eng::string name = sb.read_string(); bool persistent = sb.read_bool(); AnimValue &value = map_[name]; value.persistent = persistent; decode_value(&sb, &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; decode_value(&sb, &value); } else { decode_value(&sb, &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 == T_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 ", vtname(def.type)); } return ""; } eng::string AnimState::add_defaults(const AnimState *other) { eng::string err; AnimValue defval; defval.set_xyz(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("stdbp"); err = add_default("bp", defval, other); if (!err.empty()) return err; defval.set_string("stdmodel"); err = add_default("model", defval, other); if (!err.empty()) return err; return ""; } eng::string AnimState::apply_lua(LuaCoreStack &LS0, LuaSlot tab, bool setpersist) { LuaVar key, val, tmp; LuaExtStack LS(LS0.state(), key, val, tmp); util::DXYZ xyz; if (!LS.istable(tab)) { return "An 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."; } AnimValue parsedvalue = parse_anim_value(LS, val, tmp); if (parsedvalue.type == T_UNINITIALIZED) { return "in animation key-value pairs, val must be number, string, boolean, or xyz"; } eng::string name = LS.ckstring(key); AnimValue &mapentry = map_[name]; if ((mapentry.type != T_UNINITIALIZED) && (mapentry.type != parsedvalue.type)) { return util::ss("animation '", name, "' must be a ", vtname(mapentry.type)); } mapentry.copy_value(parsedvalue); if (setpersist) mapentry.persistent = true; } return ""; } void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent) { LuaVar name, val; LuaExtStack LS(LS0.state(), name, val); LS.newtable(tab); for (const auto &pair : map_) { if (pair.second.persistent != persistent) continue; LS.set(name, pair.first); const AnimValue &value = pair.second; if (value.type == T_BOOLEAN) { LS.set(val, value.get_boolean()); } else if (value.type == T_NUMBER) { LS.set(val, value.get_number()); } else if (value.type == T_STRING) { LS.set(val, value.get_string()); } else if (value.type == T_XYZ) { LS.newtable(val); LS.rawset(val, 1, value.get_xyz().x); LS.rawset(val, 2, value.get_xyz().y); LS.rawset(val, 3, value.get_xyz().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_ascii_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(); decode_value(&sb, &value); if (persistent) { if ((name == "xyz") && (value.type == T_XYZ)) xyz = value.xyz; if ((name == "plane") && (value.type == T_STRING)) plane = value.str; } } } // Just return the hash of the very last step. (Steps are stored last to first). static uint64_t encqueue_final_hash(std::string_view encqueue) { StreamBuffer sb(encqueue); uint64_t hash = sb.read_uint64(); return hash; } // Return the encstep for the final step of the animation queue. static std::string_view encqueue_final_encstep(std::string_view encqueue) { StreamBuffer sb(encqueue); sb.read_uint64(); std::string_view result = sb.read_string_view(); return result; } // Return the encqueue for the first N steps. // If there aren't that many steps, then just return them all. std::string_view encqueue_finaln(std::string_view encqueue, int n) { StreamBuffer sb(encqueue); while ((n > 0) && (!sb.empty())) { sb.read_uint64(); uint64_t slen = sb.read_length(); sb.read_bytes(slen); n -= 1; } return encqueue.substr(0, sb.total_reads()); } AnimQueue::AnimQueue() { size_limit_ = 10; // Default size limit. clear(); } void AnimQueue::clear() { AnimState state; clear(state); } void AnimQueue::clear(const AnimState &state) { StreamBuffer result; eng::string encstep = state.encode(); uint64_t hash = hash_encstep(0, encstep); result.write_uint64(hash); result.write_string(encstep); encqueue_ = std::make_shared(result.view()); } void AnimQueue::set_limit(int nkeep) { assert((nkeep >= 2) && (nkeep <= 250)); size_limit_ = nkeep; encqueue_ = std::make_shared(encqueue_finaln(*encqueue_, nkeep)); } void AnimQueue::add(const AnimState &state) { uint64_t previoushash = encqueue_final_hash(*encqueue_); eng::string encstep = state.encode(); uint64_t hash = hash_encstep(previoushash, encstep); StreamBuffer result; result.write_uint64(hash); result.write_string(encstep); result.write_bytes(encqueue_finaln(*encqueue_, size_limit_ - 1)); encqueue_ = std::make_shared(result.view()); } void AnimQueue::serialize(StreamBuffer *sb) const { sb->write_uint8(size_limit_); sb->write_string(*encqueue_); } void AnimQueue::deserialize(StreamBuffer *sb) { size_limit_ = sb->read_uint8(); 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_uint8(255); return false; } // TODO: maybe send less data? sb->write_uint8(0); sb->write_uint32(auth.size_limit_); sb->write_string(*auth.encqueue_); return true; } void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) { int nsteps = sb->read_uint8(); if (nsteps == 255) { return; } DebugLine(dbc) << "AnimQueue modified"; size_limit_ = sb->read_uint32(); std::string_view steps = sb->read_string_view(); encqueue_ = std::make_shared(steps); } bool AnimQueue::exactly_equal(const AnimQueue &other) const { if (size_limit_ != other.size_limit_) return false; if (*encqueue_ != *other.encqueue_) return false; return true; } bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const { if (size_limit_ != other.size_limit_) return false; if (encqueue_->size() != other.encqueue_->size()) return false; if (encqueue_->compare(0, 8, *other.encqueue_) != 0) return false; return true; } void AnimQueue::print_debug_string(eng::ostringstream &oss, bool full) const { bool first = true; if (full) { oss << "limit=" << size_limit(); first = false; } // Break out the steps. eng::vector encsteps; StreamBuffer sb(*encqueue_); while (!sb.empty()) { sb.read_uint64(); encsteps.push_back(sb.read_string_view()); } 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 = encqueue_final_encstep(*encqueue_); AnimCoreState result; result.decode(encstep); return result; } AnimState AnimQueue::get_final_persistent() const { std::string_view encstep = encqueue_final_encstep(*encqueue_); AnimState result; result.decode_persistent(encstep); return result; } AnimState AnimQueue::get_final_everything() const { std::string_view encstep = encqueue_final_encstep(*encqueue_); 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_xyz("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; }