diff --git a/luprex/cpp/core/animqueue.cpp b/luprex/cpp/core/animqueue.cpp index 6f486a27..0b16469d 100644 --- a/luprex/cpp/core/animqueue.cpp +++ b/luprex/cpp/core/animqueue.cpp @@ -7,671 +7,769 @@ #include #include - -AnimStep::AnimStep() { - clear(); -} - -AnimStep::~AnimStep() {} +#include -void AnimStep::clear() { - id_ = 0; - bits_ = 0; - action_ = ""; - facing_ = 0; - xyz_ = util::XYZ(0,0,0); - graphic_ = ""; - plane_ = ""; -} - -void AnimStep::set_action(const eng::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 eng::string &g) { - bits_ |= HAS_GRAPHIC; - graphic_ = g; -} - -void AnimStep::set_plane(const eng::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(); +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"; } } -void AnimStep::config_store_string(lua_State *L, int idx, eng::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; +uint64_t hash_encoding(uint64_t prev, std::string_view s) { + return util::hash_string(util::HashValue(123, prev), s).first; } -void AnimStep::config_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 AnimValue::set_boolean(bool b) { + type = T_BOOLEAN; + str.clear(); + xyz = (b ? 1.0:0.0); } -void AnimStep::configure(LuaKeywordParser &kp, const AnimStep &aqback) { - lua_State *L = kp.state(); - LuaVar value; - LuaExtStack LS(L, value); +void AnimValue::set_number(double n) { + type = T_NUMBER; + str.clear(); + xyz = util::DXYZ(n, 0, 0); +} - if (kp.parse(value, "action")) { - config_store_string(L, value.index(), &action_, 0, "action"); - } - if (kp.parse(value, "graphic")) { - config_store_string(L, value.index(), &graphic_, HAS_GRAPHIC, "graphic"); - } - if (kp.parse(value, "plane")) { - config_store_string(L, value.index(), &plane_, HAS_PLANE, "plane"); - } - if (kp.parse(value, "x")) { - config_store_number(L, value.index(), &xyz_.x, 0.0, HAS_X, "X coordinate"); - } - if (kp.parse(value, "y")) { - config_store_number(L, value.index(), &xyz_.y, 0.0, HAS_Y, "Z coordinate"); - } - if (kp.parse(value, "z")) { - config_store_number(L, value.index(), &xyz_.z, 0.0, HAS_Z, "Z coordinate"); - } - if (kp.parse(value, "dx")) { - config_store_number(L, value.index(), &xyz_.x, aqback.xyz().x, HAS_X, "X coordinate"); - } - if (kp.parse(value, "dy")) { - config_store_number(L, value.index(), &xyz_.y, aqback.xyz().y, HAS_Y, "Y coordinate"); - } - if (kp.parse(value, "dz")) { - config_store_number(L, value.index(), &xyz_.z, aqback.xyz().z, HAS_Z, "Z coordinate"); - } - if (kp.parse(value, "facing")) { - config_store_number(L, value.index(), &facing_, 0.0, HAS_FACING, "facing"); +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); } } -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_; +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); } } -bool AnimStep::echoes(const AnimStep &prev) const { - if (!has_facing() && (facing_ != prev.facing_)) { - return 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; } - if (!has_x() && (xyz_.x != prev.xyz_.x)) { - return false; + // 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; } - if (!has_y() && (xyz_.y != prev.xyz_.y)) { - return false; + // 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 (!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; + // If it doesn't parse as any of the above, it's a string. + v->set_string(vstr); } -eng::string AnimStep::debug_string() const { +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; - 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(); - } + 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(); +} -bool AnimStep::from_string(const eng::string &config) { - clear(); - util::StringVec parts = util::split(config, ' '); - for (int i = 0; i < int(parts.size()); i++) { - const eng::string &part = parts[i]; - if (part == "") continue; - util::StringVec lr = util::split(part, '='); - if (lr.size() != 2) return false; - const eng::string &key = lr[0]; - const eng::string &val = lr[1]; - if (key == "action") { - action_ = val; - } else if (key == "id") { - int64_t id = sv::to_int64(val, -1); - if (id < 0) return false; - id_ = id; - } else if (key == "plane") { - set_plane(val); - } else if (key == "x") { - double v = sv::to_double(val); - if (std::isnan(v)) return false; - set_x(v); - } else if (key == "y") { - double v = sv::to_double(val); - if (std::isnan(v)) return false; - set_y(v); - } else if (key == "z") { - double v = sv::to_double(val); - if (std::isnan(v)) return false; - set_z(v); - } else if (key == "facing") { - double v = sv::to_double(val); - if (std::isnan(v)) return false; - set_facing(v); - } else if (key == "graphic") { - set_graphic(val); +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 { - return false; + decode_value(&sb, &dummy); } } - return true; } -AnimQueue::AnimQueue(WorldType wt) { - version_autoinc_ = (wt == WORLD_TYPE_MASTER); +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 ""; +} + + +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; +} + + +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; + } + } +} + +AnimQueue::AnimQueue() { size_limit_ = 10; // Default size limit. - steps_.emplace_back(); - steps_.front().keep_state_only(); - version_number_ = version_autoinc_ ? 1 : 0; + clear(); } -void AnimQueue::mutated() { - if (version_autoinc_) { - version_number_ += 1; - } else { - version_number_ = 0; - } +void AnimQueue::clear() { + AnimState state; + clear(state); } -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); +void AnimQueue::clear(const AnimState &state) { steps_.clear(); - steps_.emplace_back(); - steps_.front().keep_state_only(); - size_limit_ = n; - version_number_ = version_autoinc_ ? 1 : 0; -} - -void AnimQueue::clear(const eng::string &plane) { - steps_.clear(); - steps_.emplace_back(); - steps_.front().set_plane(plane); - steps_.front().keep_state_only(); - mutated(); + eng::string encoding = state.encode(); + uint64_t hash = hash_encoding(0, encoding); + steps_.emplace_back(encoding, hash); } void AnimQueue::set_limit(int n) { - assert(n >= 1); + assert((n >= 2) && (n <= 250)); 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; -} - -eng::string AnimQueue::steps_debug_string() const { - eng::ostringstream oss; - for (int i = 0; i < int(size()); i++) { - oss << nth(i).debug_string() << "; "; - } - return oss.str(); -} - -eng::string AnimQueue::full_debug_string() const { - eng::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::add(const AnimState &state) { + eng::string encoding = state.encode(); + uint64_t hash = hash_encoding(steps_.back().hash, encoding); + steps_.emplace_back(encoding, hash); + set_limit(size_limit_); } 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); + sb->write_uint64(steps_[0].hash); + for (const AnimQueue::Step &step : steps_) { + sb->write_string(step.encoding); } } void AnimQueue::deserialize(StreamBuffer *sb) { size_limit_ = sb->read_uint8(); - size_t nsteps = sb->read_uint8(); + int nsteps = sb->read_uint8(); + uint64_t firsthash = sb->read_uint64(); + assert ((nsteps >= 2) && (nsteps <= size_limit_) && (size_limit_ >= 2) && (size_limit_ <= 250)); 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]); + for (int i = 0; i < nsteps; i++) { + AnimQueue::Step &step = steps_[i]; + step.encoding = sb->read_string(); + if (i == 0) { + step.hash = firsthash; + } else { + step.hash = hash_encoding(steps_[i-1].hash, step.encoding); + } } - 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)); + // 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; } - // 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); + sb->write_uint64(auth.steps_.front().hash); + + // Index all known elements by text. + eng::map index; + for (int i = 0; i < int(steps_.size()); i++) { + index[steps_[i].encoding] = i; } - // Index the remaining elements by id. - eng::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()); + // Write the encoded elements. + for (int i = 0; i < int(auth.steps_.size()); i++) { + const AnimQueue::Step &step = auth.steps_[i]; + auto iter = index.find(step.encoding); if (iter == index.end()) { sb->write_uint8(255); - step.write_into(sb); + sb->write_string(step.encoding); } 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); - } + sb->write_uint8(iter->second); } } return true; } void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) { - int len = sb->read_uint8(); - if (len == 255) { + int nsteps = sb->read_uint8(); + if (nsteps == 255) { return; } DebugLine(dbc) << "AnimQueue modified"; size_limit_ = sb->read_uint8(); + uint64_t firsthash = sb->read_uint64(); + + assert ((nsteps >= 2) && (nsteps <= size_limit_) && (size_limit_ >= 2) && (size_limit_ <= 250)); + // Decode the diff, stop at eof. - eng::deque old = std::move(steps_); - steps_.clear(); - for (int i = 0; i < len; i++) { + eng::deque old = std::move(steps_); + steps_.resize(nsteps); + for (int i = 0; i < nsteps; i++) { + AnimQueue::Step &step = steps_[i]; uint8_t index = sb->read_uint8(); if (index < 255) { assert(index < old.size()); - steps_.push_back(old[index]); + step.encoding = old[index].encoding; } else { - AnimStep step; - step.read_from(sb); - steps_.push_back(step); + step.encoding = sb->read_string(); } - int size = steps_.size(); - if (size > 1) { - steps_[size-1].echo(steps_[size-2]); + if (i == 0) { + step.hash = firsthash; } else { - steps_[0].keep_state_only(); + step.hash = hash_encoding(steps_[i-1].hash, step.encoding); } } - mutated(); } -void AnimQueue::update_version(const AnimQueue &auth) { - assert(size_and_steps_equal(auth)); - version_number_ = auth.version_number_; +bool AnimQueue::exactly_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++) { + const AnimQueue::Step &step = steps_[i]; + const AnimQueue::Step &otherstep = other.steps_[i]; + if (step.hash != otherstep.hash) return false; + if (step.encoding != otherstep.encoding) return false; + } + return true; } -const AnimStep &AnimQueue::back() const { - return steps_.back(); +bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const { + if (size_limit_ != other.size_limit_) return false; + if (steps_.size() != other.steps_.size()) return false; + if (steps_.back().hash != other.steps_.back().hash) return false; + return true; } -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); +eng::string AnimQueue::steps_debug_string() const { + eng::ostringstream oss; + bool first = true; + for (const AnimQueue::Step &step : steps_) { + if (!first) oss << "; "; + first = false; + AnimState state(step.encoding); + state.print_debug_string(oss); + } + return oss.str(); } +eng::string AnimQueue::full_debug_string() const { + eng::ostringstream oss; + oss << "limit=" << size_limit(); + for (const AnimQueue::Step &step : steps_) { + oss << "; "; + AnimState state(step.encoding); + state.print_debug_string(oss); + } + return oss.str(); +} + +// Get the final entry, xyz and plane only. +// +AnimCoreState AnimQueue::get_final_core_state() const { + AnimCoreState result; + result.decode(steps_.back().encoding); + return result; +} + +// Get the final entry, all persistent variables. +// +AnimState AnimQueue::get_final_persistent() const { + AnimState result; + result.decode_persistent(steps_.back().encoding); + return result; +} + +// Get the final entry, everything persistent and non-persistent. +// +AnimState AnimQueue::get_final_everything() const { + AnimState result; + result.decode(steps_.back().encoding); + return result; +} + + LuaDefine(unittests_animqueue, "", "some unit tests") { // Useful objects. - AnimStep stp; - AnimQueue aq(WORLD_TYPE_MASTER); - AnimQueue aqds(WORLD_TYPE_PREDICTIVE); + AnimQueue aq, aqs; StreamBuffer sb; + AnimState astate; + eng::string enc; + AnimCoreState core; // 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"); + LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]"); - // 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"); + // 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"); - // 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 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 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); + // 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); - 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); + // 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"); - // 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)); + // 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"); - // Discard all but the last action. - aq.set_limit(1); - LuaAssert(L, diff_works(aq, aqds)); + // 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"); + LuaAssert(L, difflen2 < (difflen1 / 2)); return 0; } diff --git a/luprex/cpp/core/animqueue.hpp b/luprex/cpp/core/animqueue.hpp index e9d16433..8a3e1d7f 100644 --- a/luprex/cpp/core/animqueue.hpp +++ b/luprex/cpp/core/animqueue.hpp @@ -5,40 +5,26 @@ // An animation queue is a fifo queue of animation steps. New animations are // pushed on the back, and old ones are popped from the front. // -// An animation step has an "action" which is usually the name of an animation, -// or it's a special token like "walk" or "warp." Each animation step shows the -// resulting AnimState that the player finds himself in after executing the -// action. +// An animation step is a set of key-value pairs, where each key is an +// identifier, and each value is either a number, a boolean, an XYZ coordinate, +// or a string. A key-value pair can be either persistent or nonpersistent. +// So a typical animation step might be: // -// The first step in an animation queue always has id=0 and action="". This step -// represents the initial state of the sprite before any animations or -// movements. +// action=walk [nonpersistent] +// xyz=3,4,5 [persistent] +// facing=320 [persistent] +// plane=earth [persistent] // -// To add new items to the AnimQueue, use this process: first, call add(id, -// action). This adds a new step to the queue. Then, call set_xyz, set_facing, -// set_plane, or an other setter. These setters are meant to only be used -// immediately after calling 'add' to populate the new step. +// Persistent values are retained from one animation step to the next, +// nonpersistent values exist for one animation step only. // -// VERSION NUMBERS +// Animation steps are stored encoded as strings, which is convenient for +// passing the data to unreal, but it means that the animation step has to be +// decoded whenever you want access to the key-value pairs. // -// The version number field: if the version number in the synchronous model is -// equal to the version number in the master model, then the two animqueues are -// guaranteed to be equal. Here's how we achieve that invariant: -// -// * In the master model, the version number starts at 1 and is auto-incremented -// every time the animation queue is mutated. -// -// * In the synchronous model, the version number is set to zero every time the -// animation queue is mutated. Note that master version numbers are never -// zero. Applying a patch also sets the version number to zero. -// -// * Serializing and deserializing causes the version number to be saved and -// restored in both master and synchronous models. -// -// * The routine 'update_version' should be called after difference -// transmission. This copies the version number from the master to the -// synchronous model. This is guaranteed to be safe because we just finished -// difference transmission, therefore, the queues should match. +// Each animation step has a hash value. The hash value is generated +// by mixing the hash value of the previous step with the hash value +// of the encoded string of key-value pairs. // /////////////////////////////////////////////////////////////////// @@ -50,6 +36,7 @@ #include "wrap-deque.hpp" #include "wrap-unordered-map.hpp" +#include "luastack.hpp" #include "streambuffer.hpp" #include "debugcollector.hpp" #include "util.hpp" @@ -57,184 +44,226 @@ #include #include -class AnimStep : public eng::nevernew { - friend class AnimQueue; -public: - enum { - HAS_FACING = 1, - HAS_X = 2, - HAS_Y = 4, - HAS_Z = 8, - HAS_XYZ = 14, - HAS_GRAPHIC = 16, - HAS_PLANE = 32, - HAS_EVERYTHING = 63, - }; +enum AnimValueType { + T_STRING, + T_NUMBER, + T_BOOLEAN, + T_XYZ, + T_UNINITIALIZED +}; - AnimStep(); - ~AnimStep(); - - int64_t id() const { return id_; } - int bits() const { return bits_; } - - const eng::string &action() const { return action_; } - double facing() const { return facing_; } - float x() const { return xyz_.x; } - float y() const { return xyz_.y; } - float z() const { return xyz_.z; } - const util::XYZ &xyz() const { return xyz_; } - const eng::string &graphic() const { return graphic_; } - const eng::string &plane() const { return plane_; } - - bool has_facing() const { return bits_ & HAS_FACING; } - bool has_x() const { return bits_ & HAS_X; } - bool has_y() const { return bits_ & HAS_Y; } - bool has_z() const { return bits_ & HAS_Z; } - bool has_xyz() const { return (bits_ & HAS_XYZ) == HAS_XYZ; } - bool has_graphic() const { return bits_ & AnimStep::HAS_GRAPHIC; } - bool has_plane() const { return bits_ & AnimStep::HAS_PLANE; } - - void set_action(const eng::string &action); - void set_facing(float f); - void set_x(float f); - void set_y(float f); - void set_z(float z); - void set_xyz(const util::XYZ &xyz); - void set_graphic(const eng::string &g); - void set_plane(const eng::string &p); - - void clear(); - - // ExactlyEqual compares all fields. - bool exactly_equal(const AnimStep &other) const; - - // LogicallyEqual only compares fields whose HAS_XXX bits are set. - bool logically_equal(const AnimStep &other) const; - - // StateEqual is true if the plane, graphic, facing, and xyz match. - bool state_equal(const AnimStep &other) const; - - // read/write the step using a stream buffer. - // - void write_into(StreamBuffer *sb) const; - void read_from(StreamBuffer *sb); - - // Create an AnimStep from a lua table. - // - // Lua stack must contain a table, which may contain: - // action: "action" - // facing: 0.0 - 360.0 - // x: x-coordinate - // y: y-coordinate - // z: z-coordinate - // graphic: "graphic" - // plane: "plane" - // - // aqback: the animation queue back, from which relative - // moves are computed. - // - void configure(LuaKeywordParser &kp, const AnimStep &aqback); - - // Make this step into a first-step of an anim queue. - void keep_state_only(); +struct AnimValue { + AnimValue() : persistent(false), type(T_UNINITIALIZED) {} - // For any values that are unchanged in this step, - // echo the values of the previous step. - void echo(const AnimStep &prev); + bool persistent; + AnimValueType type; + eng::string str; + util::DXYZ xyz; - // Verify that this step echoes the previous step. - bool echoes(const AnimStep &prev) const; + // These set the type, str, and xyz fields in a consistent way. + void set_boolean(bool b); + void set_number(double n); + void set_xyz(const util::DXYZ &xyz); + void set_string(std::string_view s); - // Convert to a string for debugging purposes. - eng::string debug_string() const; + // The get the type, str, and xyz fields in a consistent way. + bool get_boolean() const; + double get_number() const; + const util::DXYZ &get_xyz() const; + std::string_view get_string() const; - // Convert a string to an animstep (for testing only). - bool from_string(const eng::string &s); - -private: - int64_t id_; - int16_t bits_; - eng::string action_; - - float facing_; - util::XYZ xyz_; - eng::string graphic_; - eng::string plane_; - - void config_store_string(lua_State *L, int idx, eng::string *target, int16_t bits, const char *name); - void config_store_number(lua_State *L, int idx, float *target, float offset, int16_t bits, const char *name); + // Copy the value from another animvalue. Don't copy the persistent flag. + void copy_value(const AnimValue &other); }; -class AnimQueue : public eng::nevernew { -public: - // World type determines whether versions increment or autozero - AnimQueue(WorldType wt); - - // Simple getters. - const AnimStep &nth(int n) const { return steps_[n]; } - size_t size() const { return steps_.size(); } - int32_t size_limit() const { return size_limit_; } - int64_t version_number() const { return version_number_; } - bool version_identical(const AnimQueue &aq) const; +class AnimState +{ +private: + using Map = eng::map; + Map map_; - // Return true if the size limit and steps are identical. - bool size_and_steps_equal(const AnimQueue &aq) const; - - // Mutator to create a new step. - void add(int64_t id, const AnimStep &step); - - // Clear and set the plane. - void clear(const eng::string &plane); - - // Serialize or deserialize to a StreamBuffer + // Set the default value, internal // - // Caution: version numbers are not stored. On deserialize, - // version number is set to zero. + eng::string add_default(const eng::string &name, const AnimValue &v, const AnimState *other); + +public: + // Clear everything + // + void clear() { map_.clear(); } + + // Set the persistent flag on a single variable. + // If the variable isn't present, add it. + // + void set_persistent(const eng::string &name); + + // Return if it contains a value for the specified name. + // + bool contains(const eng::string &name) { return map_.find(name) != map_.end(); } + + // Get the value of a specific variable. + // If the variable isn't present, return a default value. + // + bool get_boolean(const eng::string &name); + double get_number(const eng::string &name); + util::DXYZ get_xyz(const eng::string &name); + std::string_view get_string(const eng::string &name); + + // Set a single variable to a value of a specified type. + // If the variable isn't present, add it. + // + void set_boolean(const eng::string &name, bool v); + void set_number(const eng::string &name, double v); + void set_xyz(const eng::string &name, const util::DXYZ &value); + void set_string(const eng::string &name, std::string_view value); + + // Print a debug string into the stringstream. + // + void print_debug_string(eng::ostringstream &oss); + + // Return the debug string. + eng::string debug_string(); + + // Constructs an empty state. + // + AnimState() {} + + // Convert to an encoded string. + // + eng::string encode() const; + + // Decode an encoded string. + // + void decode(std::string_view enc); + + // Decode an encoded string, discarding non-persistent data. + // + void decode_persistent(std::string_view enc); + + // Add default values for all builtin persistent variables. + // + // For each builtin default (plane, xyz, facing, bp, model) + // + // - Will generate an error if a value is already present, + // but the present value is of the wrong type. + // + // - If 'other' is not nullptr, then we look in 'other' for a default + // value. If a valid value of the correct type is present, it is + // used as the default value. + // + // - If no default value can be found in 'other', then a hardwired + // default value is provided. + // + eng::string add_defaults(const AnimState *other); + + // Apply a configuration from a lua table. + // + // If a key already exists in the state, then type type from the table + // must match the type from the existing state. + // + eng::string apply_lua(LuaCoreStack &LS0, LuaSlot tab, bool setpersist); + + // Convert an animstate to a lua table. + // + // You can either convert the persistent key-value pairs, or the + // nonpersistent. So you'll need two lua tables to store both. + // + void to_lua(LuaCoreStack &LS, LuaSlot tab, bool persistent); + + // Parse a string, for unit testing. + // + void parse(std::string_view s); + void clear_and_parse(std::string_view s); + + // Constructor from an encoded string. + // + AnimState(std::string_view s) { decode(s); } +}; + +struct AnimCoreState +{ + util::DXYZ xyz; + eng::string plane; + + void decode(std::string_view enc); +}; + +class AnimQueue : public eng::nevernew { +private: + struct Step { + Step() { hash=0; } + Step(const eng::string &e, uint64_t h) : encoding(e), hash(h) {} + eng::string encoding; + uint64_t hash; + }; + +public: + // Construct an empty animation queue. + // clears the state to a valid state. + // + AnimQueue(); + + // Size limit. + // + int32_t size_limit() const { return size_limit_; } + + // Clear and set the initial state. + // + void clear(); + void clear(const AnimState &initial); + + // Set the size limit. Must be 2-250 + // + void set_limit(int n); + + // Add an animation step. + // + // Note: add does not automatically compose the step with the previous + // step, you have to do that yourself. + // + void add(const AnimState &state); + + // Serialize or deserialize to a StreamBuffer // void serialize(StreamBuffer *sb) const; void deserialize(StreamBuffer *sb); // Difference transmission + // bool diff(const AnimQueue &auth, StreamBuffer *sb) const; void patch(StreamBuffer *sb, DebugCollector *dbc); - void update_version(const AnimQueue &auth); - - // Get the final resting place after all animations are complete. - const AnimStep &back() const; -public: - //////////////////////////////////////////////////////////////////////////// + // Check for exactly equal. This does the full check of all + // fields, it is used exclusively for debugging. // - // TESTING SUPPORT - // - // The following functions are not designed to be useful for production - // code, they're designed to be helpful for unit testing. - // - //////////////////////////////////////////////////////////////////////////// + bool exactly_equal(const AnimQueue &aq) const; - // Change the size limit. - void full_clear_and_set_limit(int szl); - void set_limit(int szl); - - // Make sure the invariants are preserved. - bool valid() const; + // Check for exactly equal (fast). Compares the size, limit, + // and hash of the last entry. If these are equal, then the whole + // thing should be equal. + // + bool exactly_equal_fast(const AnimQueue &aq) const; - // Convert to a string for debugging purposes. + // Debug strings. + // eng::string steps_debug_string() const; eng::string full_debug_string() const; + + // Get the final entry, xyz and plane only. + // + AnimCoreState get_final_core_state() const; + + // Get the final entry, all persistent variables. + // + AnimState get_final_persistent() const; + + // Get the final entry, everything persistent and non-persistent. + // + AnimState get_final_everything() const; - // Convert to a - private: - bool version_autoinc_; - int32_t size_limit_; - eng::deque steps_; - int64_t version_number_; - - // Called whenever the animation queue is mutated. - // serialization and deserialization - void mutated(); + int size_limit_; + eng::deque steps_; }; #endif // ANIMQUEUE_HPP diff --git a/luprex/cpp/core/drivenengine.cpp b/luprex/cpp/core/drivenengine.cpp index 6473e63d..2d3a2188 100644 --- a/luprex/cpp/core/drivenengine.cpp +++ b/luprex/cpp/core/drivenengine.cpp @@ -2,6 +2,7 @@ #include "wrap-vector.hpp" #include "util.hpp" #include "drivenengine.hpp" +#include "world.hpp" #include #include @@ -138,6 +139,11 @@ void DrivenEngine::rescan_lua_source() { rescan_lua_source_ = true; } +void DrivenEngine::set_visible_world_and_actor(World *w, int64_t id) { + visible_world_ = w; + visible_actor_id_ = id; +} + void DrivenEngine::stop_driver() { stop_driver_ = true; for (int i = 0; i < DRV_MAX_CHAN; i++) { @@ -190,8 +196,8 @@ inline static const char *action_string(DrvAction act) { case PLAY_RECV_INCOMING: return "PLAY_RECV_INCOMING"; case PLAY_NOTIFY_CLOSE: return "PLAY_NOTIFY_CLOSE"; case PLAY_NOTIFY_ACCEPT: return "PLAY_NOTIFY_ACCEPT"; - case PLAY_SET_LUA_SOURCE: return "PLAY_SET_LUA_SOURCE"; case PLAY_INVOKE_EVENT_UPDATE: return "PLAY_INVOKE_EVENT_UPDATE"; + case PLAY_SET_LUA_SOURCE: return "PLAY_SET_LUA_SOURCE"; case PLAY_RELEASE: return "PLAY_RELEASE"; default: return "unknown"; } @@ -403,6 +409,30 @@ bool DrivenEngine::drv_get_stop_driver() const { return stop_driver_; } +uint64_t DrivenEngine::drv_get_actor_id() const { + return visible_actor_id_; +} + +void DrivenEngine::drv_get_tangibles_near(uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids) { + scan_result_.clear(); + if ((visible_world_ != 0) && (tanid != 0)) { + PlaneScan scan; + scan.set_near(tanid, true); + scan.set_omit_nowhere(true); + scan.set_sorted(false); + scan.set_radius(util::XYZ(rx, ry, rz)); + scan.set_shape(PlaneScan::CYLINDER); + visible_world_->get_near(scan, &scan_result_); + } + *count = scan_result_.size(); + if (*count > 0) { + *ids = &scan_result_[0]; + } else { + *ids = nullptr; + } +} + + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // @@ -465,6 +495,8 @@ void DrivenEngine::drv_set_lua_source(uint32_t srcpklen, const char *srcpk) { rescan_lua_source_ = false; } + + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // @@ -522,6 +554,15 @@ static bool drv_get_stop_driver(EngineWrapper *w) { return w->engine->drv_get_stop_driver(); } +static uint64_t drv_get_actor_id(EngineWrapper *w) { + return w->engine->drv_get_actor_id(); +} + +static void drv_get_tangibles_near(EngineWrapper *w, uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids) { + return w->engine->drv_get_tangibles_near(tanid, rx, ry, rz, count, ids); +} + + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // @@ -798,6 +839,7 @@ void replay_set_lua_source(EngineWrapper *w) { w->engine->drv_set_lua_source(srcpack.size(), srcpack.c_str()); } + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // @@ -864,21 +906,13 @@ static void replaycore_step(EngineWrapper *w) { case PLAY_RECV_INCOMING: replay_recv_incoming(w); return; case PLAY_NOTIFY_CLOSE: replay_notify_close(w); return; case PLAY_NOTIFY_ACCEPT: replay_notify_accept(w); return; - case PLAY_SET_LUA_SOURCE: replay_set_lua_source(w); return; case PLAY_INVOKE_EVENT_UPDATE: replay_invoke_event_update(w); return; + case PLAY_SET_LUA_SOURCE: replay_set_lua_source(w); return; case PLAY_RELEASE: release(w); return; default: return reset_wrapper(w, "Replay log corrupt in command dispatcher"); } } -////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// -// -// General Mutators -// -////////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// @@ -923,6 +957,8 @@ static void init_engine_wrapper_helper(EngineWrapper *w) { w->get_clock = drv_get_clock; w->get_rescan_lua_source = drv_get_rescan_lua_source; w->get_stop_driver = drv_get_stop_driver; + w->get_actor_id = drv_get_actor_id; + w->get_tangibles_near = drv_get_tangibles_near; w->play_initialize = play_initialize; w->play_clear_new_outgoing = play_clear_new_outgoing; diff --git a/luprex/cpp/core/drivenengine.hpp b/luprex/cpp/core/drivenengine.hpp index 41ebcf86..882a3adc 100644 --- a/luprex/cpp/core/drivenengine.hpp +++ b/luprex/cpp/core/drivenengine.hpp @@ -52,8 +52,10 @@ #include "util.hpp" #include "streambuffer.hpp" #include "enginewrapper.hpp" +#include "planemap.hpp" class DrivenEngine; +class World; using UniqueDrivenEngine = std::unique_ptr; using DrivenEngineMaker = UniqueDrivenEngine (*)(); using DrivenEngineInitializer = void (*)(); @@ -222,6 +224,14 @@ public: // void rescan_lua_source(); + // Set the world pointer and the actor ID. + // + // This allows the graphics engine to query the DrivenEngine + // about the state of the world and the player. It is legal to set these + // to zero, in which case queries will return null results. + // + void set_visible_world_and_actor(World *w, int64_t actor); + // Stop the driver. The engine should call this when it's done // and there's nothing left to do. // @@ -270,6 +280,8 @@ public: double drv_get_clock() const; bool drv_get_rescan_lua_source() const; bool drv_get_stop_driver() const; + uint64_t drv_get_actor_id() const; + void drv_get_tangibles_near(uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids); void drv_initialize(uint32_t srcpklen, const char *srcpk, int argc, char **argv); void drv_clear_new_outgoing(); @@ -279,7 +291,7 @@ public: uint32_t drv_notify_accept(uint32_t port); void drv_invoke_event_update(double clock); void drv_set_lua_source(uint32_t srcpklen, const char *srcpk); - + private: // Find a currently-unused channel ID. Channel IDs // are small integers that are reused. @@ -296,6 +308,9 @@ private: eng::vector new_outgoing_; util::LuaSourcePtr lua_source_; eng::vector listen_ports_; + World *visible_world_; + int64_t visible_actor_id_; + util::IdVector scan_result_; bool rescan_lua_source_; double clock_; bool stop_driver_; diff --git a/luprex/cpp/core/enginewrapper.hpp b/luprex/cpp/core/enginewrapper.hpp index fee34661..81a1abd0 100644 --- a/luprex/cpp/core/enginewrapper.hpp +++ b/luprex/cpp/core/enginewrapper.hpp @@ -110,6 +110,14 @@ struct EngineWrapper { // bool (*get_stop_driver)(EngineWrapper *w); + // Get the actor ID. May return zero if the server is down. + // + uint64_t (*get_actor_id)(EngineWrapper *w); + + // Get the results of the last scan radius. + // + void (*get_tangibles_near)(EngineWrapper *w, uint64_t tanid, double rx, double ry, double rz, uint32_t *count, int64_t **ids); + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // diff --git a/luprex/cpp/core/lpxserver.cpp b/luprex/cpp/core/lpxserver.cpp index bd2ab855..18b12d87 100644 --- a/luprex/cpp/core/lpxserver.cpp +++ b/luprex/cpp/core/lpxserver.cpp @@ -52,6 +52,9 @@ public: // Set the console prompt. set_console_prompt(console_.get_prompt()); + + // Export stuff to the graphics engine. + set_visible_world_and_actor(master_.get(), admin_id_); } void do_luainvoke_command(const util::StringVec &words) { diff --git a/luprex/cpp/core/luaconsole.cpp b/luprex/cpp/core/luaconsole.cpp index 7d1e44b2..4825f8da 100644 --- a/luprex/cpp/core/luaconsole.cpp +++ b/luprex/cpp/core/luaconsole.cpp @@ -94,6 +94,10 @@ void LuaConsole::simplify(const StringVec &words) { if (words.size() != 1) { synerr("/work takes no arguments"); } + } else if (words[0] == "display") { + if (words.size() != 1) { + synerr("/display takes no arguments"); + } } else if (words[0] == "aborthttp") { if (words.size() != 1) { synerr("/aborthttp takes no arguments"); diff --git a/luprex/cpp/core/planemap.cpp b/luprex/cpp/core/planemap.cpp index 7012f386..3423a67b 100644 --- a/luprex/cpp/core/planemap.cpp +++ b/luprex/cpp/core/planemap.cpp @@ -224,13 +224,12 @@ public: using NodeID = uint64_t; using ChildBits = uint64_t; - using IdVector = util::IdVector; // The PlaneMap that this tree is a part of. PlaneMap *planemap_; // The name of this plane. - eng::string plane_; + std::string plane_; // Internal nodes in the tree just have bits indicating // which children exist. @@ -440,7 +439,7 @@ public: (*os) << "| "; print_node_id(node, os); (*os) << " "; - util::IdVector ids; + IdVector ids; collect_planeitem_ids(first, &ids); std::sort(ids.begin(), ids.end()); util::print_id_vector(ids, os); @@ -667,7 +666,7 @@ public: } // Construct a PlaneTree. - PlaneTree(PlaneMap *pmap, const eng::string &plane) { + PlaneTree(PlaneMap *pmap, std::string_view plane) { planemap_ = pmap; plane_ = plane; total_count_ = 0; @@ -779,30 +778,29 @@ PlaneMap::PlaneMap() : default_radius_(32768.0) {} PlaneMap::~PlaneMap() {} -IdVector PlaneMap::scan(const PlaneScan &sc) const { - IdVector result; +void PlaneMap::scan(const PlaneScan &sc, util::IdVector *into) const { + into->clear(); int startpos = 0; if (sc.near_ != 0) { if (sc.include_near_) { - result.push_back(sc.near_); + into->push_back(sc.near_); startpos = 1; } } if (sc.omit_nowhere_ && (sc.plane_ == "nowhere")) { - return result; + return; } - auto piter = planes_.find(sc.plane_); + auto piter = planes_.find(std::string_view(sc.plane_)); if (piter != planes_.end()) { const std::unique_ptr &tree = piter->second; - tree->scan(sc, &result, nullptr); + tree->scan(sc, into, nullptr); } if (sc.sorted_) { - std::sort(result.begin() + startpos, result.end()); + std::sort(into->begin() + startpos, into->end()); } - return result; } eng::string PlaneMap::tree_debug_string(const eng::string &plane) { @@ -814,11 +812,11 @@ eng::string PlaneMap::outliers_debug_string(const eng::string &plane) { } eng::string PlaneMap::search_bboxes_debug_string(const PlaneScan &scan) { - return PlaneTree::get(this, scan.plane_)->search_bboxes_debug_string(scan); + return PlaneTree::get(this, eng::string(scan.plane_))->search_bboxes_debug_string(scan); } eng::string PlaneMap::scan_steps_debug_string(const PlaneScan &scan) { - return PlaneTree::get(this, scan.plane_)->scan_steps_debug_string(scan); + return PlaneTree::get(this, eng::string(scan.plane_))->scan_steps_debug_string(scan); } void PlaneMap::untrack_all() { @@ -1161,7 +1159,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // The two corners are deliberately not in low-high order. scan.clear(); scan.set_plane("p"); - scan.set_bbox_given_center_radius(util::XYZ(0x23, 0x97, 0x103), 2.0f); + scan.set_center_and_radius(util::XYZ(0x23, 0x97, 0x103), 2.0f); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" @@ -1181,7 +1179,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // sure they only include the one cell. scan.clear(); scan.set_plane("p"); - scan.set_bbox_given_center_radius(util::XYZ(0x12, 0x34, 0x45), 0.0); + scan.set_center_and_radius(util::XYZ(0x12, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" @@ -1207,7 +1205,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // from one cell to the next. scan.clear(); scan.set_plane("p"); - scan.set_bbox_given_center_radius(util::XYZ(0x12 + 0.5, 0x34, 0x45), 0.0); + scan.set_center_and_radius(util::XYZ(0x12 + 0.5, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 8,8,8 - 8,8,8" @@ -1235,7 +1233,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // to make sure they cover the entire PlaneTree. scan.clear(); scan.set_plane("p"); - scan.set_bbox_given_center_radius(util::XYZ(0x12, 0x34, 0x45), 100000.0); + scan.set_center_and_radius(util::XYZ(0x12, 0x34, 0x45), 100000.0); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 0,0,0 - f,f,f" @@ -1292,7 +1290,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { scan.clear(); scan.set_plane("p"); scan.set_shape(PlaneScan::SPHERE); - scan.set_bbox_given_center_radius(util::XYZ(0x12 + 0.1, 0x34, 0x45), 0.0); + scan.set_center_and_radius(util::XYZ(0x12 + 0.1, 0x34, 0x45), 0.0); LuaAssertStrEq(L, pm.scan_steps_debug_string(scan), "|L8:root" "| L7:2,2,2" @@ -1350,7 +1348,7 @@ LuaDefine(unittests_planemap, "", "some unit tests") { // because the radius is nonzero. scan.clear(); scan.set_plane("p"); - scan.set_bbox_given_center_radius(util::XYZ(0x100000, 0x16, 0x23), 0.2); + scan.set_center_and_radius(util::XYZ(0x100000, 0x16, 0x23), 0.2); LuaAssertStrEq(L, pm.search_bboxes_debug_string(scan), "|Level 8 0,0,0 - 0,0,0" "|Level 6 f,8,8 - f,8,8" diff --git a/luprex/cpp/core/planemap.hpp b/luprex/cpp/core/planemap.hpp index 1a8cac50..c6d62c8f 100644 --- a/luprex/cpp/core/planemap.hpp +++ b/luprex/cpp/core/planemap.hpp @@ -87,6 +87,7 @@ class PlaneMap; class PlaneTree; + class PlaneScan : public eng::nevernew { public: friend class PlaneMap; @@ -94,7 +95,11 @@ public: enum Shape { BOX, CYLINDER, SPHERE }; private: // The plane to scan. - eng::string plane_; + // + // Note: the reason this uses std::string instead of eng::string + // is that we want plane scans to not touch the engine heap. + // + std::string plane_; // The bounding box of the scan. util::XYZ center_, radius_; @@ -119,13 +124,14 @@ private: public: void clear() { - plane_ = ""; + plane_.clear(); shape_ = BOX; sorted_ = true; near_ = 0; include_near_ = false; omit_nowhere_ = false; - radius_ = center_ = util::XYZ(); + radius_ = util::XYZ(); + center_ = util::XYZ(); } PlaneScan() { clear(); } @@ -143,7 +149,7 @@ public: // void configure(LuaKeywordParser &kw); - void set_bbox_given_center_radius(const util::XYZ ¢er, float r) { + void set_center_and_radius(const util::XYZ ¢er, float r) { set_center(center); set_radius(r); } @@ -204,12 +210,12 @@ public: class PlaneMap : public eng::nevernew { friend class PlaneItem; friend class PlaneTree; -public: - using IdVector = util::IdVector; - private: float default_radius_; - eng::map> planes_; + + // Plane Trees table. + // + eng::map, std::less<>> planes_; public: // No special code is needed for construction or destruction. @@ -220,7 +226,11 @@ public: // PlaneItem. See PlaneItem::track and PlaneItem::untrack. // Scan the PlaneMap for items, return their IDs. - IdVector scan(const PlaneScan &s) const; + // + // Note: the reason this uses IdVector instead of IdVector + // is that we want scans to not touch the engine heap. + // + void scan(const PlaneScan &sc, util::IdVector *into) const; // Set the default radius for all planes. // Maybe we'll make it adaptive some day. diff --git a/luprex/cpp/core/streambuffer.cpp b/luprex/cpp/core/streambuffer.cpp index f9497b6b..e11dbdde 100644 --- a/luprex/cpp/core/streambuffer.cpp +++ b/luprex/cpp/core/streambuffer.cpp @@ -281,6 +281,26 @@ void StreamBuffer::write_double(double d) { write_cursor_ += 8; } +void StreamBuffer::write_xyz(const util::XYZ &xyz) { + make_space(12); + memcpy(write_cursor_, &xyz.x, 4); + write_cursor_ += 4; + memcpy(write_cursor_, &xyz.y, 4); + write_cursor_ += 4; + memcpy(write_cursor_, &xyz.z, 4); + write_cursor_ += 4; +} + +void StreamBuffer::write_dxyz(const util::DXYZ &xyz) { + make_space(24); + memcpy(write_cursor_, &xyz.x, 8); + write_cursor_ += 8; + memcpy(write_cursor_, &xyz.y, 8); + write_cursor_ += 8; + memcpy(write_cursor_, &xyz.z, 8); + write_cursor_ += 8; +} + int8_t StreamBuffer::read_int8() { check_available(1); int8_t v; @@ -336,6 +356,32 @@ double StreamBuffer::read_double() { return d; } +util::XYZ StreamBuffer::read_xyz() { + check_available(12); + util::XYZ result; + memcpy(&result.x, read_cursor_, 4); + read_cursor_ += 4; + memcpy(&result.y, read_cursor_, 4); + read_cursor_ += 4; + memcpy(&result.z, read_cursor_, 4); + read_cursor_ += 4; + return result; +} + +util::DXYZ StreamBuffer::read_dxyz() { + check_available(24); + util::DXYZ result; + memcpy(&result.x, read_cursor_, 8); + read_cursor_ += 8; + memcpy(&result.y, read_cursor_, 8); + read_cursor_ += 8; + memcpy(&result.z, read_cursor_, 8); + read_cursor_ += 8; + return result; +} + + + void StreamBuffer::write_hashvalue(const util::HashValue &hv) { write_uint64(hv.first); write_uint64(hv.second); diff --git a/luprex/cpp/core/streambuffer.hpp b/luprex/cpp/core/streambuffer.hpp index 9b3243ba..2865ba5e 100644 --- a/luprex/cpp/core/streambuffer.hpp +++ b/luprex/cpp/core/streambuffer.hpp @@ -313,6 +313,8 @@ public: void write_char(char c); void write_float(float f); void write_double(double d); + void write_xyz(const util::XYZ &xyz); + void write_dxyz(const util::DXYZ &xyz); // Read fixed-size integers from the buffer. // @@ -329,7 +331,9 @@ public: char read_char(); float read_float(); double read_double(); - + util::XYZ read_xyz(); + util::DXYZ read_dxyz(); + // Write other types into the buffer. // // Note that strings are preceded by a length field. Reading diff --git a/luprex/cpp/core/util.cpp b/luprex/cpp/core/util.cpp index a7fffb6e..b721f1a1 100644 --- a/luprex/cpp/core/util.cpp +++ b/luprex/cpp/core/util.cpp @@ -493,6 +493,15 @@ void print_id_vector(const IdVector &idv, std::ostream *os) { first = false; } } + +void print_id_vector(const std::vector &idv, std::ostream *os) { + bool first = true; + for (int64_t id : idv) { + if (!first) (*os) << ","; + (*os) << id; + first = false; + } +} eng::string id_vector_debug_string(const IdVector &idv) { eng::ostringstream oss; @@ -518,10 +527,17 @@ IdVector sort_union_id_vectors(const IdVector &v1, const IdVector &v2) { return result; } -HashValue hash_string(const eng::string &s) { +HashValue hash_string(std::string_view s) { uint64_t hash1 = 0; uint64_t hash2 = 0; - SpookyHash::ChainHash128(s.c_str(), s.size(), &hash1, &hash2); + SpookyHash::ChainHash128(s.data(), s.size(), &hash1, &hash2); + return util::HashValue(hash1, hash2); +} + +HashValue hash_string(HashValue prev, std::string_view s) { + uint64_t hash1 = prev.first; + uint64_t hash2 = prev.second; + SpookyHash::ChainHash128(s.data(), s.size(), &hash1, &hash2); return util::HashValue(hash1, hash2); } @@ -718,11 +734,6 @@ LuaSourcePtr make_lua_source(const eng::string &code) { return result; } -eng::string XYZ::debug_string() const { - eng::ostringstream oss; - oss << "(" << x << "," << y << "," << z << ")"; - return oss.str(); -} void (*dprint_hook)(const char *oneline, size_t size); diff --git a/luprex/cpp/core/util.hpp b/luprex/cpp/core/util.hpp index 60bb14a0..e44376df 100644 --- a/luprex/cpp/core/util.hpp +++ b/luprex/cpp/core/util.hpp @@ -211,13 +211,18 @@ enum MessageType { MSG_INVOKE, }; +// Note: IdVector is weird in that it deliberately uses std::vector +// instead of eng::vector. This is because we want plane scans +// to not touch the engine heap. +// +using IdVector = std::vector; + using StringVec = eng::vector; using StringPair = std::pair; using StringSet = eng::set; using LuaSourceVec = eng::vector; using LuaSourcePtr = std::unique_ptr; using HashValue = std::pair; -using IdVector = eng::vector; // Ascii uppercase and lowercase. eng::string ascii_tolower(std::string_view c); @@ -241,6 +246,7 @@ IdVector id_vector_create(int64_t id1=-1, int64_t id2=-1, int64_t id3=-1, int64_ // Print an ID vector to a stream. void print_id_vector(const IdVector &idv, std::ostream *os); +void print_id_vector(const std::vector &idv, std::ostream *os); // ID vector debug string. eng::string id_vector_debug_string(const IdVector &idv); @@ -249,7 +255,10 @@ eng::string id_vector_debug_string(const IdVector &idv); IdVector sort_union_id_vectors(const IdVector &v1, const IdVector &v2); // Get a 128-bit hashvalue for a string. -HashValue hash_string(const eng::string &str); +HashValue hash_string(std::string_view str); + +// Get a 128-bit hashvalue for a string, with a previous value. +HashValue hash_string(HashValue prev, std::string_view str); // Get a 128-bit hashvalue for an ID vector. HashValue hash_id_vector(const IdVector &idv); @@ -322,18 +331,32 @@ void remove_marked_items(T &vec) { } // An XYZ coordinate, general purpose. -struct XYZ { - float x, y, z; - XYZ() { x=0; y=0; z=0; } - XYZ(float ix, float iy, float iz) { x=ix; y=iy; z=iz; } - bool operator ==(const XYZ &o) const { return x==o.x && y == o.y && z==o.z; } - bool operator !=(const XYZ &o) const { return x!=o.x || y != o.y || z!=o.z; } - XYZ operator -(const XYZ &o) const { return XYZ(x-o.x, y-o.y, z-o.z); } - XYZ operator +(const XYZ &o) const { return XYZ(x+o.x, y+o.y, z+o.z); } - XYZ operator *(float scale) const { return XYZ(x*scale, y*scale, z*scale); } - eng::string debug_string() const; +template +struct NumXYZ { + using Number = NUMBER; + Number x, y, z; + NumXYZ() { x=0; y=0; z=0; } + NumXYZ(Number ix, Number iy, Number iz) { x=ix; y=iy; z=iz; } + void operator =(const NumXYZ &other) { x = other.x; y = other.y; z = other.z; } + void operator =(Number n) { x = n; y = n; z = n; } + bool operator ==(const NumXYZ &o) const { return x==o.x && y == o.y && z==o.z; } + bool operator !=(const NumXYZ &o) const { return x!=o.x || y != o.y || z!=o.z; } + NumXYZ operator -(const NumXYZ &o) const { return NumXYZ(x-o.x, y-o.y, z-o.z); } + NumXYZ operator +(const NumXYZ &o) const { return NumXYZ(x+o.x, y+o.y, z+o.z); } + NumXYZ operator *(float scale) const { return NumXYZ(x*scale, y*scale, z*scale); } + template + const NumXYZ convert() const { NumXYZ r; r.x=ONUMBER(x); r.y=ONUMBER(y); r.z=ONUMBER(z); return r; } + + eng::string debug_string() const { + eng::ostringstream oss; + oss << "(" << x << "," << y << "," << z << ")"; + return oss.str(); + } }; +using XYZ=NumXYZ; +using DXYZ=NumXYZ; + // util::ostringstream // // This is a variant of ostringstream in which it is possible @@ -455,4 +478,9 @@ inline std::ostream &operator<<(std::ostream &oss, const util::XYZ &xyz) { return oss; } +inline std::ostream &operator<<(std::ostream &oss, const util::DXYZ &xyz) { + oss << xyz.x << "," << xyz.y << "," << xyz.z; + return oss; +} + #endif // UTIL_HPP diff --git a/luprex/cpp/core/world-accessor.cpp b/luprex/cpp/core/world-accessor.cpp index 0a9cf46e..6a026888 100644 --- a/luprex/cpp/core/world-accessor.cpp +++ b/luprex/cpp/core/world-accessor.cpp @@ -17,56 +17,169 @@ static void tangible_getall(LuaCoreStack &LS0, LuaSlot list, const util::IdVecto } } -LuaDefine(tangible_animstate, "tan", - "|Get the entire animation state of the tangible." - "|Returns six values: graphic,plane,x,y,z,facing.") { - LuaArg tanobj; - LuaRet graphic, plane, x, y, z, facing; - LuaDefStack LS(L, tanobj, graphic, plane, x, y, z, facing); - World *w = World::fetch_global_pointer(L); - Tangible *tan = w->tangible_get(LS, tanobj, false); - const AnimStep &aqback = tan->anim_queue_.back(); - LS.set(graphic, aqback.graphic()); - LS.set(plane, aqback.plane()); - LS.set(x, aqback.xyz().x); - LS.set(y, aqback.xyz().y); - LS.set(z, aqback.xyz().z); - LS.set(facing, aqback.facing()); - return LS.result(); -} - LuaDefine(tangible_xyz, "tan", - "|Get the current coordinates of the tangible." - "|Returns three values: x, y, z") { + "|Get the current coordinates of the tangible and the plane." + "|Returns four values: x, y, z, plane") { LuaArg tanobj; - LuaRet x, y, z; + LuaRet x, y, z, plane; LuaDefStack LS(L, tanobj, x, y, z); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(LS, tanobj, false); - const AnimStep &aqback = tan->anim_queue_.back(); - LS.set(x, aqback.xyz().x); - LS.set(y, aqback.xyz().y); - LS.set(z, aqback.xyz().z); + AnimCoreState pos = tan->anim_queue_.get_final_core_state(); + LS.set(x, pos.xyz.x); + LS.set(y, pos.xyz.y); + LS.set(z, pos.xyz.z); + LS.set(plane, pos.plane); return LS.result(); } -LuaDefine(tangible_animate, "tan,configtable", - "|Add an animation step to the tangible." - "|The configtable is a table containing any of the following:" - "|action,graphic,plane,x,y,z,facing") { - LuaArg tanobj, config; - LuaDefStack LS(L, tanobj, config); - LuaKeywordParser kp(LS, config); +LuaDefine(tangible_animdebug, "tan", + "|Return a debug string showing the entire animation queue" + "|") { + LuaArg tanobj; + LuaRet result; + LuaDefStack LS(L, tanobj, result); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(LS, tanobj, false); - int64_t id = w->alloc_id_predictable(); - AnimStep step; - step.configure(kp, tan->anim_queue_.back()); - kp.final_check_throw(); - if (step.action() == "") { - luaL_error(L, "animation action must be specified"); + LS.set(result, tan->anim_queue_.steps_debug_string()); + return LS.result(); +} + +LuaDefine(tangible_animstate, "tan", + "|Return the animation state variables of the tangible as a table." + "|" + "|The animation system stores 'animation state variables. " + "|There are several builtin animation state variables. The" + "|following is a list, along with some example values that" + "|might be used if the object were a pirate treasure chest." + "|" + "| xyz={1,2,3} # xyz coordinate" + "| plane='earth' # plane where the chest is located" + "| facing=0 # rotation of the chest" + "| bp='BP_piratechest' # name of an unreal blueprint" + "| model='SM_piratechest' # name of an unreal mesh" + "|" + "|There can also be user-defined animation state variables." + "|For example, for a pirate chest, you might want to add two" + "|more state variables:" + "|" + "| open=true # chest can be open or closed" + "| fullness=0.8 # how big the heap of coins is" + "|" + "|All state variables must be one of four types: number, string," + "|boolean, or coordinate. All state variables must have simple" + "|identifiers for names." + "|" + "|Animation state variables are updated when you use" + "|tangible.animate to create an animation record. For example," + "|suppose your character is initialized at xyz={1,1,1}. Then" + "|he walks to xyz={2,2,2}, then he walks to xyz={3,3,3}." + "|The animation queue now contains three animation steps:" + "|" + "|Animation Queue:" + "| Step 1: action:initialize xyz={1,1,1} ..." + "| Step 2: action:walkto xyz={2,2,2} ..." + "| Step 3: action:walkto xyz={3,3,3} ..." + "|" + "|Notice that the state variable xyz is stored three times, once" + "|in each animation step. All animation state variables are stored" + "|in every animation step. When you use tangible.animstate to fetch" + "|the current value of the animation state variables, you're" + "|actually fetching the state variables from the last step in" + "|the animation queue." + "|" + "|You can use this function, tangible.animstate, to view the" + "|current set of state variables. You can use tangible.animinit" + "|to reconfigure the set of user-defined state variables. You" + "|can use tangible.animate to create animation steps, which can" + "|alter any of the state variables." + "|") { + LuaArg tanobj; + LuaRet result; + LuaDefStack LS(L, tanobj, result); + World *w = World::fetch_global_pointer(L); + Tangible *tan = w->tangible_get(LS, tanobj, false); + AnimState state = tan->anim_queue_.get_final_persistent(); + state.to_lua(LS, result, true); + return LS.result(); +} + +LuaDefine(tangible_animinit, "tan,config", + "|Reinitialize the animation queue." + "|" + "|The animation queue stores certain animation state variables." + "|See doc(tangible.animstate) for more information about animation" + "|state variables." + "|" + "|This function, tangible.animinit, is used to reconfigure the set of" + "|user-defined state variables. The config table that you pass in must" + "|be key-value pairs. Keys must be simple identifiers. Values can be" + "|numbers, strings, booleans, or coordinates." + "|" + "|After tangible.animinit, the tangible's animation state variables will" + "|consist of the user-defined variables listed in the config table," + "|plus all the builtin animation state variables." + "|" + "|Optionally, the config table can also supply values for some or all" + "|of the builtin state variables. For example, you could supply xyz" + "|in the config table. If you do, the tangible will move to a new xyz" + "|coordinate. If not, the tangible will retain its old xyz coordinate." + "|") { + LuaArg tanobj, config; + LuaDefStack LS(L, tanobj, config); + World *w = World::fetch_global_pointer(L); + Tangible *tan = w->tangible_get(LS, tanobj, false); + AnimState state; + eng::string error = state.apply_lua(LS, config, true); + if (!error.empty()) { + luaL_error(L, "%s", error.c_str()); } - tan->anim_queue_.add(id, step); + AnimState defsource = tan->anim_queue_.get_final_persistent(); + error = state.add_defaults(&defsource); + if (!error.empty()) { + luaL_error(L, "%s", error.c_str()); + } + tan->anim_queue_.clear(state); + tan->update_plane_item(); + return LS.result(); +} + + +LuaDefine(tangible_animate, "tan,config", + "|Add an animation step to the tangible." + "|" + "|The animation queue stores animation steps. This function, " + "|tangible.animate, adds one new animation step to the queue." + "|" + "|It might be useful to read about 'animation state variables' before" + "|reading this explanation. See doc(tangible.animstate)." + "|" + "|An animation step is just a list of key-value pairs. Therefore," + "|the config table must be key-value pairs. Keys must be simple" + "|identifiers. Values can be numbers, strings, booleans, or" + "|coordinates." + "|" + "|Some of the key-value pairs may match the name of an animation state" + "|variable. If so, that key-value pair permanently changes the" + "|value of that animation state variable. The new value will" + "|be retained for all future animation steps." + "|" + "|Some of the key-value pairs may NOT match the name of any animation" + "|state variable. If so, that key-value pair is part of the" + "|animation step, but nothing is propagated forward to future animation" + "|steps." + "|" + "|") { + LuaArg tanobj, config; + LuaDefStack LS(L, tanobj, config); + World *w = World::fetch_global_pointer(L); + Tangible *tan = w->tangible_get(LS, tanobj, false); + AnimState state = tan->anim_queue_.get_final_persistent(); + eng::string error = state.apply_lua(LS, config, false); + if (!error.empty()) { + luaL_error(L, "%s", error.c_str()); + } + tan->anim_queue_.add(state); tan->update_plane_item(); return LS.result(); } @@ -132,51 +245,53 @@ LuaDefine(tangible_delete, "tan", LuaDefine(tangible_build, "config", "|Build a new tangible object." - "|The config table must contain: class,x,y,z,plane,graphic." - "|The config table can optionally contain: facing.") { + "|" + "|The config table must contain: class,animstate." + "|" ){ LuaArg config; - LuaVar classname, classtab, mt; + LuaVar classname, classtab, mt, animstate; LuaRet database; - LuaDefStack LS(L, config, classname, classtab, database, mt); + LuaDefStack LS(L, config, classname, classtab, database, mt, animstate); LuaKeywordParser kp(LS, config); - // Get the class of the new tangible. + // Get the keyword arguments. if (!kp.parse(classname, "class")) { luaL_error(L, "You must specify a class for the tangible"); } + if (!kp.parse(animstate, "animstate")) { + luaL_error(L, "You must specify an animstate table"); + } + kp.final_check_throw(); + + // Find the class. eng::string err = LS.getclass(classtab, classname); if (err != "") { luaL_error(L, "%s", err.c_str()); } - // Parse the initial animation step. - AnimStep initstep, blank; - initstep.configure(kp, blank); - kp.final_check_throw(); - if (!initstep.has_xyz()) { - luaL_error(L, "You must specify (X,Y,Z) for new tangible"); + // Calculate the initial animation state. + AnimState state; + err = state.apply_lua(LS, animstate, true); + if (err != "") { + luaL_error(L, "%s", err.c_str()); } - if (!initstep.has_plane()) { - luaL_error(L, "You must specify plane for new tangible"); + if (!state.contains("xyz") || !state.contains("plane")) { + luaL_error(L, "You must specify both xyz and plane in animstate"); } - if (!initstep.has_graphic()) { - luaL_error(L, "You must specify graphic for new tangible"); + err = state.add_defaults(nullptr); + if (err != "") { + luaL_error(L, "%s", err.c_str()); } - - // TODO: generate error if there's extra crap in the config table. - + World *w = World::fetch_global_pointer(L); int64_t new_id = w->alloc_id_predictable(); - Tangible *tan = w->tangible_make(LS, database, new_id, "nowhere"); + Tangible *tan = w->tangible_make(LS, database, new_id); // Update the class of the new tangible. LS.getmetatable(mt, database); LS.rawset(mt, "__index", classtab); - - // Update the animation queue and planemap of the new tangible. - int64_t stepid = w->alloc_id_predictable(); - tan->anim_queue_.add(stepid, initstep); - tan->update_plane_item(); + + tan->anim_queue_.clear(state); return LS.result(); } @@ -259,34 +374,27 @@ LuaDefine(tangible_place, "", } LuaDefine(tangible_near, "tan,radius,omit_nowhere,omit_self", - "|Scan near the specified tangible." - "|If omit_nowhere is true, and the tangible is on the nowhere plane," - "|then the scan returns empty. If omit_self is true, then the " - "|tangible passed in is omitted from the results.") { + "|Deprecated. Use tangible.find instead.") { LuaArg ltan, lradius, lomit_nowhere, lomit_self; LuaRet list; LuaDefStack LS(L, ltan, lradius, lomit_nowhere, lomit_self, list); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(LS, ltan, false); - const AnimStep &aqback = tan->anim_queue_.back(); PlaneScan scan; - scan.set_plane(aqback.plane()); - scan.set_bbox_given_center_radius(aqback.xyz(), LS.cknumber(lradius)); + scan.set_radius(LS.cknumber(lradius)); scan.set_shape(PlaneScan::SPHERE); scan.set_sorted(true); scan.set_near(tan->id(), !LS.ckboolean(lomit_self)); scan.set_omit_nowhere(LS.ckboolean(lomit_nowhere)); - - util::IdVector idv = w->plane_map_.scan(scan); + util::IdVector idv; + w->get_near(scan, &idv); tangible_getall(LS, list, idv); return LS.result(); } LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere", - "|Scan the specified plane." - "|If omit_nowhere is true, and the plane is 'nowhere', then" - "|the scan returns empty.") { + "|Deprecated. Use tangible.find instead.") { LuaArg lplane, lx, ly, lradius, lomit_nowhere; LuaRet list; LuaDefStack LS(L, lplane, lx, ly, lradius, lomit_nowhere, list); @@ -294,12 +402,13 @@ LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere", PlaneScan scan; scan.set_plane(LS.ckstring(lplane)); - scan.set_bbox_given_center_radius(util::XYZ(LS.cknumber(lx), LS.cknumber(ly), 0), LS.cknumber(lradius)); + scan.set_center_and_radius(util::XYZ(LS.cknumber(lx), LS.cknumber(ly), 0), LS.cknumber(lradius)); scan.set_shape(PlaneScan::SPHERE); scan.set_sorted(true); scan.set_omit_nowhere(LS.ckboolean(lomit_nowhere)); - util::IdVector idv = w->plane_map_.scan(scan); + util::IdVector idv; + w->get_near(scan, &idv); tangible_getall(LS, list, idv); return LS.result(); } @@ -369,21 +478,9 @@ LuaDefine(tangible_find, "config", scan.configure(kw); kw.final_check_throw(); - // When the configure routine sees the 'near' flag, it stores the tangible - // ID, but not the center and plane, because doing so would require it to - // know about world models. We have to handle center and plane for 'near' - // separately. World *w = World::fetch_global_pointer(L); - int64_t near = scan.near(); - if (near != 0) { - Tangible *t = w->tangible_get(near); - assert(t != nullptr); // Should never happen. - const AnimStep &aqback = t->anim_queue_.back(); - scan.set_plane(aqback.plane()); - scan.set_center(aqback.xyz()); - } - // Do the scan. - util::IdVector idv = w->plane_map_.scan(scan); + util::IdVector idv; + w->get_near(scan, &idv); tangible_getall(LS, result, idv); return LS.result(); } diff --git a/luprex/cpp/core/world-core.cpp b/luprex/cpp/core/world-core.cpp index e167a4db..8a77eac0 100644 --- a/luprex/cpp/core/world-core.cpp +++ b/luprex/cpp/core/world-core.cpp @@ -77,14 +77,14 @@ World::World(WorldType wt) { assign_seqno_ = 1; } -Tangible::Tangible(World *w, int64_t id) : world_(w), anim_queue_(w->world_type_), id_player_pool_(&w->id_global_pool_) { +Tangible::Tangible(World *w, int64_t id) : world_(w), anim_queue_(), id_player_pool_(&w->id_global_pool_) { plane_item_.set_id(id); plane_item_.track(&w->plane_map_); } void Tangible::update_plane_item() { - const AnimStep &aqback = anim_queue_.back(); - plane_item_.set_pos(aqback.plane(), aqback.xyz().x, aqback.xyz().y, aqback.xyz().z); + AnimCoreState pos = anim_queue_.get_final_core_state(); + plane_item_.set_pos(pos.plane, pos.xyz.x, pos.xyz.y, pos.xyz.z); } void Tangible::serialize(StreamBuffer *sb) { @@ -140,7 +140,7 @@ Tangible *World::tangible_get(const LuaCoreStack &LS, LuaSlot tab, bool allowdel return result; } -Tangible *World::tangible_make(const LuaCoreStack &LS0, LuaSlot database, int64_t id, const eng::string &plane) { +Tangible *World::tangible_make(const LuaCoreStack &LS0, LuaSlot database, int64_t id) { assert(id != 0); LuaVar metatab; LuaExtStack LS(LS0.state(), metatab); @@ -150,8 +150,10 @@ Tangible *World::tangible_make(const LuaCoreStack &LS0, LuaSlot database, int64_ assert (t == nullptr); t.reset(new Tangible(this, id)); - // Set up initial animation state. - t->anim_queue_.clear(plane); + // AnimQueue initializes itself to a valid default state. + AnimState state; + state.add_defaults(nullptr); + t->anim_queue_.clear(state); t->update_plane_item(); // Fetch the tangible's Lua database and metatable. @@ -165,10 +167,10 @@ Tangible *World::tangible_make(const LuaCoreStack &LS0, LuaSlot database, int64_ return t.get(); } -Tangible *World::tangible_make(int64_t id, const eng::string &plane) { +Tangible *World::tangible_make(int64_t id) { LuaVar database; LuaExtStack LS(state(), database); - return tangible_make(LS, database, id, plane); + return tangible_make(LS, database, id); } void World::tangible_delete(int64_t id) { @@ -199,23 +201,33 @@ void World::tangible_delete(int64_t id) { tangibles_.erase(iter); } -util::IdVector World::get_near(int64_t player_id, float radius, bool exclude_nowhere, bool omit_player, bool sorted) const { - const Tangible *player = tangible_get(player_id); - if (player == nullptr) { - return IdVector(); +void World::get_near(PlaneScan &scan, util::IdVector *into) const { + uint32_t hash1 = eng::memhash(); + into->clear(); + // If 'near' is set, update the plane and center. + int64_t actor_id = scan.near(); + if (actor_id != 0) { + const Tangible *player = tangible_get(actor_id); + if (player == nullptr) { + return; + } + const PlaneItem &pi = player->plane_item_; + scan.set_plane(pi.plane()); + scan.set_center(util::XYZ(pi.x(), pi.y(), pi.z())); } + plane_map_.scan(scan, into); + uint32_t hash2 = eng::memhash(); + assert(hash1 == hash2); +} - // Find out where the player is. - const AnimStep &aqback = player->anim_queue_.back(); - +void World::get_near(int64_t player_id, float radius, bool exclude_nowhere, bool omit_player, bool sorted, util::IdVector *into) const { PlaneScan scan; - scan.set_plane(aqback.plane()); - scan.set_bbox_given_center_radius(aqback.xyz(), radius); + scan.set_radius(radius); scan.set_shape(PlaneScan::SPHERE); scan.set_sorted(sorted); scan.set_omit_nowhere(exclude_nowhere); scan.set_near(player_id, !omit_player); - return plane_map_.scan(scan); + get_near(scan, into); } World::Redirects World::fetch_redirects() { @@ -229,7 +241,7 @@ int64_t World::create_login_actor() { int64_t id = id_global_pool_.get_one(); LuaVar database, classtab, mt; LuaExtStack LS(state(), database, classtab, mt); - Tangible *tan = tangible_make(LS, database, id, "nowhere"); + Tangible *tan = tangible_make(LS, database, id); LS.makeclass(classtab, "login"); LS.getmetatable(mt, database); LS.rawset(mt, "__index", classtab); diff --git a/luprex/cpp/core/world-diffxmit.cpp b/luprex/cpp/core/world-diffxmit.cpp index 2ea2854a..d86a26e8 100644 --- a/luprex/cpp/core/world-diffxmit.cpp +++ b/luprex/cpp/core/world-diffxmit.cpp @@ -3,9 +3,10 @@ #include "serializelua.hpp" util::IdVector World::get_visible_union(int64_t actor_id, World *master) { - return util::sort_union_id_vectors( - master->get_near(actor_id, RadiusVisibility, true, false, false), - get_near(actor_id, RadiusVisibility, true, false, false)); + util::IdVector v1, v2; + master->get_near(actor_id, RadiusVisibility, true, false, false, &v1); + get_near(actor_id, RadiusVisibility, true, false, false, &v2); + return util::sort_union_id_vectors(v1, v2); } int64_t World::patch_actor(StreamBuffer *sb, DebugCollector *dbc) { @@ -14,7 +15,7 @@ int64_t World::patch_actor(StreamBuffer *sb, DebugCollector *dbc) { Tangible *s_actor = tangible_get(actor_id); if (s_actor == nullptr) { DebugLine(dbc) << "create new actor " << actor_id; - s_actor = tangible_make(actor_id, ""); + s_actor = tangible_make(actor_id); s_actor->id_player_pool_.deserialize(sb); s_actor->anim_queue_.deserialize(sb); s_actor->print_buffer_.deserialize(sb); @@ -61,7 +62,7 @@ void World::patch_visible(StreamBuffer *sb, DebugCollector *dbc) { for (int i = 0; i < count; i++) { int64_t id = sb->read_int64(); DebugLine(dbc) << "patch_visible create tan " << id; - Tangible *t = tangible_make(id, ""); + Tangible *t = tangible_make(id); t->anim_queue_.deserialize(sb); t->update_plane_item(); } @@ -162,7 +163,6 @@ void World::diff_visible(const util::IdVector &visible, World *master, s_tan = tangible_get(m_tan->id()); assert(s_tan != nullptr); } - s_tan->anim_queue_.update_version(m_tan->anim_queue_); } } } @@ -172,8 +172,8 @@ void World::patch_luatabs(StreamBuffer *sb, DebugCollector *dbc) { int64_t actor_id = sb->read_int64(); util::HashValue closehash = sb->read_hashvalue(); int ncreate = sb->read_int32(); - util::IdVector closetans = - get_near(actor_id, RadiusClose, true, false, true); + util::IdVector closetans; + get_near(actor_id, RadiusClose, true, false, true, &closetans); assert(closehash == util::hash_id_vector(closetans)); number_lua_tables(closetans); create_new_tables(ncreate); @@ -188,22 +188,23 @@ void World::diff_luatabs(int64_t actor_id, World *master, StreamBuffer *xsb) { StreamBuffer tsb; // Calculate the set of close tangibles. - util::IdVector closetans = - master->get_near(actor_id, RadiusClose, true, false, true); - assert(get_near(actor_id, RadiusClose, true, false, true) == closetans); - util::HashValue closehash = util::hash_id_vector(closetans); + util::IdVector mclosetans, sclosetans; + master->get_near(actor_id, RadiusClose, true, false, true, &mclosetans); + get_near(actor_id, RadiusClose, true, false, true, &sclosetans); + assert(mclosetans == sclosetans); + util::HashValue closehash = util::hash_id_vector(mclosetans); // Number and pair tables in the synchronous and master model. - number_lua_tables(closetans); - pair_lua_tables(closetans, master->state()); - int ncreate = number_remaining_tables(closetans, master->state()); + number_lua_tables(mclosetans); + pair_lua_tables(mclosetans, master->state()); + int ncreate = number_remaining_tables(mclosetans, master->state()); create_new_tables(ncreate); // Difference transmit. tsb.write_int64(actor_id); tsb.write_hashvalue(closehash); tsb.write_int32(ncreate); - diff_tangible_databases(closetans, master->state(), &tsb); + diff_tangible_databases(mclosetans, master->state(), &tsb); diff_numbered_tables(master->state(), &tsb); // Forward to client, and apply to server-synchronous. @@ -257,8 +258,8 @@ void World::diff_tanclass(int64_t actor_id, World *master, StreamBuffer *xsb) { // Calculate the set of close tangibles. // TODO: we've already calculated this in an earlier function. This is // wasteful. - util::IdVector closetans = - master->get_near(actor_id, RadiusClose, true, false, true); + util::IdVector closetans; + master->get_near(actor_id, RadiusClose, true, false, true, &closetans); tsb.write_int32(0); int write_count_after = tsb.total_writes(); diff --git a/luprex/cpp/core/world-testing.cpp b/luprex/cpp/core/world-testing.cpp index 91ad2229..93af7e2b 100644 --- a/luprex/cpp/core/world-testing.cpp +++ b/luprex/cpp/core/world-testing.cpp @@ -3,18 +3,37 @@ #include "json.hpp" #include -void World::tangible_walkto(int64_t id, int64_t animid, float x, float y) { + +void World::tangible_clear_anim_queue_to_empty(int64_t id) { Tangible *t = tangible_get(id); - assert(animid != 0); assert(t != nullptr); - AnimStep step; - step.set_action("walkto"); - step.set_x(x); - step.set_y(y); - t->anim_queue_.add(animid, step); + AnimState state; + t->anim_queue_.clear(state); + t->update_plane_item(); } +void World::tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, const util::DXYZ &xyz) { + Tangible *t = tangible_get(id); + assert(t != nullptr); + AnimState state; + state.set_string("plane", plane); + state.set_xyz("xyz", xyz); + state.set_persistent("plane"); + state.set_persistent("xyz"); + t->anim_queue_.clear(state); + t->update_plane_item(); +} + +void World::tangible_walkto(int64_t id, float x, float y) { + Tangible *t = tangible_get(id); + assert(t != nullptr); + AnimState state = t->anim_queue_.get_final_persistent(); + state.set_string("action", "walkto"); + state.set_xyz("xyz", util::DXYZ(x,y,0.0)); + t->anim_queue_.add(state); +} + eng::string World::tangible_anim_debug_string(int64_t id) const { const Tangible *t = tangible_get(id); if (t == 0) return "no such tangible"; @@ -40,10 +59,12 @@ eng::string World::tangible_ids_debug_string() const { eng::string World::tangibles_near_debug_string(int64_t actor, int64_t distance) { eng::ostringstream result; - for (int64_t id : get_near(actor, distance, true, false, true)) { + util::IdVector tans; + get_near(actor, distance, true, false, true, &tans); + for (int64_t id : tans) { const Tangible *tan = tangible_get(id); - const AnimStep &aqback = tan->anim_queue_.back(); - result << id << ": " << aqback.graphic() << " " << aqback.plane() << " " << aqback.xyz().debug_string() << std::endl; + AnimState state = tan->anim_queue_.get_final_everything(); + result << id << ": " << state.debug_string() << std::endl; } return result.str(); } @@ -267,53 +288,35 @@ LuaDefine(unittests_world1animdiff, "", "some unit tests") { util::IdVector ids = util::id_vector_create(123, 345); // Create some tangibles, and add some animations. - m->tangible_make(123, "somewhere"); - m->tangible_make(345, "somewhere"); - m->tangible_walkto(123, 770, 3, 4); - m->tangible_walkto(345, 771, 6, 2); + m->tangible_make(123); + m->tangible_make(345); + m->tangible_clear_anim_queue_to_empty(123); + m->tangible_clear_anim_queue_to_empty(345); + m->tangible_walkto(123, 3, 4); + m->tangible_walkto(345, 6, 2); LuaAssertStrEq(L, m->tangible_ids_debug_string(), "123,345"); - LuaAssertStrEq(L, m->tangible_anim_debug_string(123), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=770 action=walkto x=3 y=4; "); - LuaAssertStrEq(L, m->tangible_anim_debug_string(345), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=771 action=walkto x=6 y=2; "); + LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0"); + LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0"); // Now difference transmit all that to the client. ss->diff_visible(ids, m.get(), &sb); cs->patch_visible(&sb, nullptr); LuaAssertStrEq(L, ss->tangible_ids_debug_string(), "123,345"); - LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=770 action=walkto x=3 y=4; "); - LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=771 action=walkto x=6 y=2; "); + LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0"); + LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0"); LuaAssert(L, worlds_identical(ss, cs)); // Now add some more animation records to the master. - m->tangible_walkto(123, 772, 7, 3); - m->tangible_walkto(345, 773, 2, 5); - LuaAssertStrEq(L, m->tangible_anim_debug_string(123), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=770 action=walkto x=3 y=4; " - "id=772 action=walkto x=7 y=3; "); - LuaAssertStrEq(L, m->tangible_anim_debug_string(345), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=771 action=walkto x=6 y=2; " - "id=773 action=walkto x=2 y=5; "); + m->tangible_walkto(123, 7, 3); + m->tangible_walkto(345, 2, 5); + LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0; action:walkto xyz:7,3,0"); + LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0; action:walkto xyz:2,5,0"); // Now difference transmit all that to the client again. ss->diff_visible(ids, m.get(), &sb); cs->patch_visible(&sb, nullptr); - LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=770 action=walkto x=3 y=4; " - "id=772 action=walkto x=7 y=3; "); - LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), - "id=0 action= plane=somewhere x=0 y=0 z=0 facing=0 graphic=; " - "id=771 action=walkto x=6 y=2; " - "id=773 action=walkto x=2 y=5; "); + LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "[empty]; action:walkto xyz:3,4,0; action:walkto xyz:7,3,0"); + LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "[empty]; action:walkto xyz:6,2,0; action:walkto xyz:2,5,0"); LuaAssert(L, worlds_identical(ss, cs)); // Delete tangible 345. @@ -337,7 +340,7 @@ LuaDefine(unittests_world2pairtab, "", "some unit tests") { // Create a master model containing some general tables, and // some specialty tables (not numberable). - m->tangible_make(123, "somewhere"); + m->tangible_make(123); m->tangible_set_string(123, "inventory.TID", "inventory"); m->tangible_set_string(123, "transactions.TID", "transactions"); m->tangible_set_string(123, "skills.TID", "skills"); @@ -348,7 +351,7 @@ LuaDefine(unittests_world2pairtab, "", "some unit tests") { // Now we're going to create a synchronous model that's similar to, but not // exactly the same as that master model. - ss->tangible_make(123, "somewhere"); + ss->tangible_make(123); ss->tangible_set_string(123, "inventory.TID", "inventory"); ss->tangible_set_string(123, "skills.TID", "skills"); ss->tangible_set_string(123, "skills.crap.TID", "skills.crap"); @@ -384,13 +387,20 @@ LuaDefine(unittests_world3diffluatab, "", "some unit tests") { StreamBuffer sb; // Initialize all three models so that a tangible exists. - m->tangible_make(123, "somewhere"); - ss->tangible_make(123, "somewhere"); - cs->tangible_make(123, "somewhere"); - m->tangible_make(345, "somewhere"); - ss->tangible_make(345, "somewhere"); - cs->tangible_make(345, "somewhere"); + m->tangible_make(123); + ss->tangible_make(123); + cs->tangible_make(123); + m->tangible_make(345); + ss->tangible_make(345); + cs->tangible_make(345); + m->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0)); + ss->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0)); + cs->tangible_clear_plane_and_xyz(123, "earth", util::DXYZ(0,0,0)); + m->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0)); + ss->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0)); + cs->tangible_clear_plane_and_xyz(345, "earth", util::DXYZ(0,0,0)); + // Put some data into the master model. m->tangible_set_string(123, "bacon", "crispy"); m->tangible_set_string(123, "inventory.gold", "wealthy"); @@ -438,9 +448,9 @@ LuaDefine(unittests_world4difftanclass, "", "some unit tests") { StreamBuffer sb; // Initialize all three models so that a tangible exists. - m->tangible_make(123, "somewhere"); - ss->tangible_make(123, "somewhere"); - cs->tangible_make(123, "somewhere"); + m->tangible_make(123); + ss->tangible_make(123); + cs->tangible_make(123); // Change the lua class of the tangible. m->tangible_set_class(123, "chicken"); diff --git a/luprex/cpp/core/world.hpp b/luprex/cpp/core/world.hpp index 2726d6c6..bb874997 100644 --- a/luprex/cpp/core/world.hpp +++ b/luprex/cpp/core/world.hpp @@ -125,16 +125,24 @@ public: // The unsorted version returns the tangibles in an unpredictable order. If sorted // is false, return them in an unpredictable order. // - IdVector get_near(int64_t player_id, float radius, bool exclude_nowhere, bool omit_player, bool sorted) const; - + void get_near(int64_t player_id, float radius, bool exclude_nowhere, bool omit_player, bool sorted, IdVector *into) const; + + // get_near + // + // Get a list of tangibles in any arbitrary region. This differs from + // PlaneMap::scan in that if the scan specifies a 'near', then the plane + // and center of the scan are updated from the near tangible. + // + void get_near(PlaneScan &sc, IdVector *into) const; + // Make a tangible. // // You must provide a valid previously-unused ID. Otherwise, leaves the lua // stack untouched. Returns a pointer to the C++ part of the tangible, and // optionally stores the Lua part in a stack slot. // - Tangible *tangible_make(const LuaCoreStack &LS0, LuaSlot tan, int64_t id, const eng::string &plane); - Tangible *tangible_make(int64_t id, const eng::string &plane); + Tangible *tangible_make(const LuaCoreStack &LS0, LuaSlot tan, int64_t id); + Tangible *tangible_make(int64_t id); // Get a pointer to the specified tangible. // @@ -362,9 +370,17 @@ public: // //////////////////////////////////////////////////////////////////////////// + // Clear the animation queue and remove all persistent state. + // + void tangible_clear_anim_queue_to_empty(int64_t id); + + // Clear the animation queue to a reasonable starting position. + // + void tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, const util::DXYZ &xyz); + // Add a 'walkto' animation to the specified tangible. // - void tangible_walkto(int64_t id, int64_t animid, float x, float y); + void tangible_walkto(int64_t id, float x, float y); // Get the tangible's animation queue as a debug string. // diff --git a/luprex/lua/uglyglobals.lua b/luprex/lua/uglyglobals.lua index 6b299929..5ff8c19d 100644 --- a/luprex/lua/uglyglobals.lua +++ b/luprex/lua/uglyglobals.lua @@ -6,7 +6,7 @@ function ug.the() lis=tangible.scan('globals',0,0,0,true) if #lis==0 then - local ugid=tangible.build{class='ug',x=0,y=0,z=0,plane='globals',graphic='box'} + local ugid=tangible.build{class='ug', animstate={xyz={0,0,0}, plane='globals'}} print("The global table is "..tangible.id(ugid)) end