Files
integration/luprex/cpp/core/animqueue.cpp

778 lines
25 KiB
C++

#include "wrap-sstream.hpp"
#include "wrap-map.hpp"
#include "util.hpp"
#include "animqueue.hpp"
#include "luastack.hpp"
#include "streambuffer.hpp"
#include <limits>
#include <cmath>
#include <cstdlib>
util::SharedStdString make_shared_string(const StreamBuffer &sb) {
return std::make_shared<std::string>(sb.view());
}
static const char *vtname(AnimValueType vt) {
switch (vt) {
case T_UNINITIALIZED: return "uninitialized";
case T_BOOLEAN: return "boolean";
case T_NUMBER: return "number";
case T_STRING: return "string";
case T_XYZ: return "xyz";
default: return "unknown";
}
}
uint64_t hash_encstep(uint64_t prev, std::string_view s) {
return util::hash_string(util::HashValue(123, prev), s).first;
}
void AnimValue::set_boolean(bool b) {
type = T_BOOLEAN;
str.clear();
xyz = (b ? 1.0:0.0);
}
void AnimValue::set_number(double n) {
type = T_NUMBER;
str.clear();
xyz = util::DXYZ(n, 0, 0);
}
void AnimValue::set_xyz(const util::DXYZ &v) {
type = T_XYZ;
str.clear();
xyz = v;
}
void AnimValue::set_string(std::string_view sv) {
type = T_STRING;
str = sv;
xyz = 0.0;
}
bool AnimValue::get_boolean() const {
if (type != T_BOOLEAN) return false;
return (xyz.x == 1.0);
}
double AnimValue::get_number() const {
if (type != T_NUMBER) return 0.0;
return xyz.x;
}
const util::DXYZ &AnimValue::get_xyz() const {
static util::DXYZ zero;
if (type != T_XYZ) return zero;
return xyz;
}
std::string_view AnimValue::get_string() const {
if (type != T_STRING) return std::string_view("");
return std::string_view(str);
}
void AnimValue::copy_value(const AnimValue &other) {
type = other.type;
str = other.str;
xyz = other.xyz;
}
static void encode_value(StreamBuffer *sb, const AnimValue *v) {
sb->write_uint8(uint8_t(v->type));
switch(v->type) {
case T_NUMBER: sb->write_double(v->xyz.x); break;
case T_BOOLEAN: sb->write_bool(v->xyz.x == 1.0); break;
case T_XYZ: sb->write_dxyz(v->xyz); break;
case T_STRING: sb->write_string(v->str); break;
default: assert(false);
}
}
static void decode_value(StreamBuffer *sb, AnimValue *v) {
AnimValueType type = AnimValueType(sb->read_uint8());
switch (type) {
case T_NUMBER: v->set_number(sb->read_double()); break;
case T_BOOLEAN: v->set_boolean(sb->read_bool()); break;
case T_XYZ: v->set_xyz(sb->read_dxyz()); break;
case T_STRING: v->set_string(sb->read_string()); break;
default: assert(false);
}
}
// Parse a value. This is meant for unit testing only. The
// parser isn't powerful enough to express all possible values.
static void parse_value(std::string_view vstr, AnimValue *v) {
// Try to interpret vstr as a boolean.
bool is_true = (vstr == "true");
bool is_false = (vstr == "false");
if (is_true || is_false) {
v->set_boolean(is_true);
return;
}
// Try to interpret vstr as a number.
if (sv::valid_number(vstr, true, true, true, false)) {
v->set_number(std::atof(std::string(vstr).c_str()));
return;
}
// Try to interpret vstr as a vector.
eng::vector<eng::string> parts = util::split(eng::string(vstr), ',');
if ((parts.size() == 3) &&
(sv::valid_number(parts[0], true, true, true, false)) &&
(sv::valid_number(parts[1], true, true, true, false)) &&
(sv::valid_number(parts[2], true, true, true, false))) {
double x = std::atof(parts[0].c_str());
double y = std::atof(parts[1].c_str());
double z = std::atof(parts[2].c_str());
v->set_xyz(util::DXYZ(x,y,z));
return;
}
// If it doesn't parse as any of the above, it's a string.
v->set_string(vstr);
}
static AnimValue parse_anim_value(LuaCoreStack &LS, LuaSlot val, LuaSlot tmp) {
AnimValue result;
if (LS.isboolean(val)) {
result.set_boolean(LS.ckboolean(val));
} else if (LS.isnumber(val)) {
result.set_number(LS.cknumber(val));
} else if (LS.isstring(val)) {
result.set_string(LS.ckstring(val));
} else if (LS.istable(val)) {
util::DXYZ xyz;
LS.rawget(tmp, val, 1);
if (!LS.isnumber(tmp)) return result;
xyz.x = LS.cknumber(tmp);
LS.rawget(tmp, val, 2);
if (!LS.isnumber(tmp)) return result;
xyz.y = LS.cknumber(tmp);
LS.rawget(tmp, val, 3);
if (!LS.isnumber(tmp)) return result;
xyz.z = LS.cknumber(tmp);
result.set_xyz(xyz);
}
return result;
}
void AnimState::set_persistent(const eng::string &name) {
map_[name].persistent = true;
}
bool AnimState::get_boolean(const eng::string &name) {
auto iter = map_.find(name);
if (iter == map_.end()) return false;
return iter->second.get_boolean();
}
double AnimState::get_number(const eng::string &name) {
auto iter = map_.find(name);
if (iter == map_.end()) return 0.0;
return iter->second.get_number();
}
util::DXYZ AnimState::get_xyz(const eng::string &name) {
static util::DXYZ zero;
auto iter = map_.find(name);
if (iter == map_.end()) return zero;
return iter->second.get_xyz();
}
std::string_view AnimState::get_string(const eng::string &name) {
auto iter = map_.find(name);
if (iter == map_.end()) return std::string_view("");
return iter->second.get_string();
}
void AnimState::set_boolean(const eng::string &name, bool v) {
AnimValue &value = map_[name];
value.set_boolean(v);
}
void AnimState::set_number(const eng::string &name, double v) {
AnimValue &value = map_[name];
value.set_number(v);
}
void AnimState::set_xyz(const eng::string &name, const util::DXYZ &v) {
AnimValue &value = map_[name];
value.set_xyz(v);
}
void AnimState::set_string(const eng::string &name, std::string_view v) {
AnimValue &value = map_[name];
value.set_string(v);
}
void AnimState::print_debug_string(eng::ostringstream &oss) {
bool first = true;
if (map_.empty()) {
oss << "[empty]";
}
for (const auto &pair : map_) {
if (!first) oss << " ";
const eng::string &name = pair.first;
const AnimValue &value = pair.second;
oss << name;
if (value.persistent) {
oss << "=";
} else {
oss << ":";
}
switch (value.type) {
case T_NUMBER: oss << value.xyz.x; break;
case T_BOOLEAN: oss << ((value.xyz.x == 1.0) ? "true":"false"); break;
case T_XYZ: oss << value.xyz; break;
case T_STRING: oss << value.str; break;
default: assert(false);
}
first = false;
}
}
eng::string AnimState::debug_string() {
eng::ostringstream oss;
print_debug_string(oss);
return oss.str();
}
eng::string AnimState::encode() const {
StreamBuffer sb;
for (const auto &pair : map_) {
const eng::string &name = pair.first;
const AnimValue &value = pair.second;
sb.write_string(name);
sb.write_bool(value.persistent);
encode_value(&sb, &value);
}
return sb.read_entire_contents();
}
void AnimState::decode(std::string_view s) {
map_.clear();
StreamBuffer sb(s);
while (!sb.empty()) {
eng::string name = sb.read_string();
bool persistent = sb.read_bool();
AnimValue &value = map_[name];
value.persistent = persistent;
decode_value(&sb, &value);
}
}
void AnimState::decode_persistent(std::string_view s) {
map_.clear();
StreamBuffer sb(s);
AnimValue dummy;
while (!sb.empty()) {
eng::string name = sb.read_string();
bool persistent = sb.read_bool();
if (persistent) {
AnimValue &value = map_[name];
value.persistent = persistent;
decode_value(&sb, &value);
} else {
decode_value(&sb, &dummy);
}
}
}
eng::string AnimState::add_default(const eng::string &name, const AnimValue &def, const AnimState *other) {
AnimValue &value = map_[name];
value.persistent = true;
if (value.type == T_UNINITIALIZED) {
if (other != nullptr) {
auto otheriter = other->map_.find(name);
if (otheriter != other->map_.end()) {
if (otheriter->second.persistent && otheriter->second.type == def.type) {
value.copy_value(otheriter->second);
return "";
}
}
}
value.copy_value(def);
return "";
}
if (value.type != def.type) {
return util::ss("Animation key ", name, " must be a ", vtname(def.type));
}
return "";
}
eng::string AnimState::add_defaults(const AnimState *other) {
eng::string err;
AnimValue defval;
defval.set_xyz(util::DXYZ(0,0,0));
err = add_default("xyz", defval, other);
if (!err.empty()) return err;
defval.set_string("nowhere");
err = add_default("plane", defval, other);
if (!err.empty()) return err;
defval.set_number(0.0);
err = add_default("facing", defval, other);
if (!err.empty()) return err;
defval.set_string("stdbp");
err = add_default("bp", defval, other);
if (!err.empty()) return err;
defval.set_string("stdmodel");
err = add_default("model", defval, other);
if (!err.empty()) return err;
return "";
}
eng::string AnimState::apply_lua(LuaCoreStack &LS0, LuaSlot tab, bool setpersist) {
LuaVar key, val, tmp;
LuaExtStack LS(LS0.state(), key, val, tmp);
util::DXYZ xyz;
if (!LS.istable(tab)) {
return "An animstate must be a table.";
}
LS.set(key, LuaNil);
while (LS.next(tab, key, val)) {
if (!LS.isstring(key)) {
return "in animation key-value pairs, key must be a string.";
}
AnimValue parsedvalue = parse_anim_value(LS, val, tmp);
if (parsedvalue.type == T_UNINITIALIZED) {
return "in animation key-value pairs, val must be number, string, boolean, or xyz";
}
eng::string name = LS.ckstring(key);
AnimValue &mapentry = map_[name];
if ((mapentry.type != T_UNINITIALIZED) && (mapentry.type != parsedvalue.type)) {
return util::ss("animation '", name, "' must be a ", vtname(mapentry.type));
}
mapentry.copy_value(parsedvalue);
if (setpersist) mapentry.persistent = true;
}
return "";
}
void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent) {
LuaVar name, val;
LuaExtStack LS(LS0.state(), name, val);
LS.newtable(tab);
for (const auto &pair : map_) {
if (pair.second.persistent != persistent) continue;
LS.set(name, pair.first);
const AnimValue &value = pair.second;
if (value.type == T_BOOLEAN) {
LS.set(val, value.get_boolean());
} else if (value.type == T_NUMBER) {
LS.set(val, value.get_number());
} else if (value.type == T_STRING) {
LS.set(val, value.get_string());
} else if (value.type == T_XYZ) {
LS.newtable(val);
LS.rawset(val, 1, value.get_xyz().x);
LS.rawset(val, 2, value.get_xyz().y);
LS.rawset(val, 3, value.get_xyz().z);
}
LS.rawset(tab, name, val);
}
}
// The syntax used by this parser is not general enough to represent all
// possible strings. That's OK, though, since it's just for unit testing.
void AnimState::parse(std::string_view config) {
while (true) {
config = sv::ltrim(config);
if (config.empty()) break;
eng::string name(sv::read_ascii_identifier(config));
assert(!name.empty());
AnimValue &value = map_[name];
bool has_equal = sv::has_prefix(config, "=");
bool has_colon = sv::has_prefix(config, ":");
assert(has_equal || has_colon);
config.remove_prefix(1);
value.persistent = has_equal;
eng::string vstr(sv::read_to_space(config));
parse_value(vstr, &value);
}
}
void AnimState::clear_and_parse(std::string_view config) {
map_.clear();
parse(config);
}
void AnimCoreState::decode(std::string_view s) {
plane.clear();
xyz = 0.0;
StreamBuffer sb(s);
AnimValue value;
while (!sb.empty()) {
eng::string name = sb.read_string();
bool persistent = sb.read_bool();
decode_value(&sb, &value);
if (persistent) {
if ((name == "xyz") && (value.type == T_XYZ)) xyz = value.xyz;
if ((name == "plane") && (value.type == T_STRING)) plane = value.str;
}
}
}
struct StepBreakout {
uint64_t hash;
std::string_view encstep;
StepBreakout(uint64_t h, std::string_view e) : hash(h), encstep(e) {}
};
using StepBreakoutVec = eng::vector<StepBreakout>;
StepBreakoutVec encqueue_breakout(std::string_view encqueue) {
StepBreakoutVec result;
StreamBuffer sb(encqueue);
while (!sb.empty()) {
uint64_t hash = sb.read_uint64();
std::string_view encstep = sb.read_string_view();
result.emplace_back(hash, encstep);
}
return result;
}
// Just return the hash of the very last step. (Steps are stored last to first).
static uint64_t encqueue_final_hash(std::string_view encqueue) {
StreamBuffer sb(encqueue);
uint64_t hash = sb.read_uint64();
return hash;
}
static std::string_view encqueue_final_encstep(std::string_view encqueue) {
StreamBuffer sb(encqueue);
sb.read_uint64();
std::string_view result = sb.read_string_view();
return result;
}
// Return the encqueue for the first N steps. If there aren't that
// many steps, then just return them all.
std::string_view encqueue_firstn(std::string_view encqueue, int n) {
StreamBuffer sb(encqueue);
while ((n > 0) && (!sb.empty())) {
sb.read_uint64();
uint64_t slen = sb.read_length();
sb.read_bytes(slen);
n -= 1;
}
return encqueue.substr(0, sb.total_reads());
}
AnimQueue::AnimQueue() {
size_limit_ = 10; // Default size limit.
clear();
}
void AnimQueue::clear() {
AnimState state;
clear(state);
}
void AnimQueue::clear(const AnimState &state) {
StreamBuffer result;
eng::string encstep = state.encode();
uint64_t hash = hash_encstep(0, encstep);
result.write_uint64(hash);
result.write_string(encstep);
encqueue_ = make_shared_string(result);
}
void AnimQueue::set_limit(int nkeep) {
assert((nkeep >= 2) && (nkeep <= 250));
size_limit_ = nkeep;
encqueue_ = std::make_shared<std::string>(encqueue_firstn(*encqueue_, nkeep));
}
void AnimQueue::add(const AnimState &state) {
uint64_t previoushash = encqueue_final_hash(*encqueue_);
eng::string encstep = state.encode();
uint64_t hash = hash_encstep(previoushash, encstep);
StreamBuffer result;
result.write_uint64(hash);
result.write_string(encstep);
result.write_bytes(encqueue_firstn(*encqueue_, size_limit_ - 1));
encqueue_ = make_shared_string(result);
}
void AnimQueue::serialize(StreamBuffer *sb) const {
sb->write_uint8(size_limit_);
sb->write_string(*encqueue_);
}
void AnimQueue::deserialize(StreamBuffer *sb) {
size_limit_ = sb->read_uint8();
encqueue_ = std::make_shared<std::string>(sb->read_string_view());
}
bool AnimQueue::diff(const AnimQueue &auth, StreamBuffer *sb) const {
// Fast check for exactly equivalent. If equivalent, skip all the work.
if (exactly_equal_fast(auth)) {
assert(exactly_equal(auth));
sb->write_uint8(255);
return false;
}
// TODO: maybe send less data?
sb->write_uint8(0);
sb->write_uint32(auth.size_limit_);
sb->write_string(*auth.encqueue_);
return true;
}
void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) {
int nsteps = sb->read_uint8();
if (nsteps == 255) {
return;
}
DebugLine(dbc) << "AnimQueue modified";
size_limit_ = sb->read_uint32();
std::string_view steps = sb->read_string_view();
encqueue_ = std::make_shared<std::string>(steps);
}
bool AnimQueue::exactly_equal(const AnimQueue &other) const {
if (size_limit_ != other.size_limit_) return false;
if (*encqueue_ != *other.encqueue_) return false;
return true;
}
bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const {
if (size_limit_ != other.size_limit_) return false;
if (encqueue_->size() != other.encqueue_->size()) return false;
if (encqueue_->compare(0, 8, *other.encqueue_) != 0) return false;
return true;
}
eng::string AnimQueue::steps_debug_string() const {
StepBreakoutVec breakout = encqueue_breakout(*encqueue_);
eng::ostringstream oss;
bool first = true;
for (int i = breakout.size() - 1; i >= 0; i--) {
const StepBreakout &step = breakout[i];
if (!first) oss << "; ";
first = false;
AnimState state(step.encstep);
state.print_debug_string(oss);
}
return oss.str();
}
eng::string AnimQueue::full_debug_string() const {
StepBreakoutVec breakout = encqueue_breakout(*encqueue_);
eng::ostringstream oss;
oss << "limit=" << size_limit();
for (int i = breakout.size() - 1; i >= 0; i--) {
const StepBreakout &step = breakout[i];
oss << "; ";
AnimState state(step.encstep);
state.print_debug_string(oss);
}
return oss.str();
}
// Get the final entry, xyz and plane only.
//
AnimCoreState AnimQueue::get_final_core_state() const {
std::string_view encstep = encqueue_final_encstep(*encqueue_);
AnimCoreState result;
result.decode(encstep);
return result;
}
// Get the final entry, all persistent variables.
//
AnimState AnimQueue::get_final_persistent() const {
std::string_view encstep = encqueue_final_encstep(*encqueue_);
AnimState result;
result.decode_persistent(encstep);
return result;
}
// Get the final entry, everything persistent and non-persistent.
//
AnimState AnimQueue::get_final_everything() const {
std::string_view encstep = encqueue_final_encstep(*encqueue_);
AnimState result;
result.decode(encstep);
return result;
}
LuaDefine(unittests_animqueue, "", "some unit tests") {
// Useful objects.
AnimQueue aq, aqs;
StreamBuffer sb;
AnimState astate;
eng::string enc;
AnimCoreState core;
// Debug string of a newly initialized queue
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
// Test AnimState simple setters.
astate.set_string("color", "blue");
astate.set_xyz("xyz", util::DXYZ(1,2,3));
astate.set_number("half", 0.5);
astate.set_boolean("nice", true);
LuaAssertStrEq(L, astate.debug_string(), "color:blue half:0.5 nice:true xyz:1,2,3");
// Test AnimState simple getters.
LuaAssert(L, astate.get_string("color") == "blue");
LuaAssert(L, astate.get_xyz("xyz") == util::DXYZ(1,2,3));
LuaAssert(L, astate.get_number("half") == 0.5);
LuaAssert(L, astate.get_boolean("nice") == true);
// Test AnimState simple getters on nonexistent data.
LuaAssert(L, astate.get_string("q") == "");
LuaAssert(L, astate.get_xyz("q") == util::DXYZ(0,0,0));
LuaAssert(L, astate.get_number("q") == 0.0);
LuaAssert(L, astate.get_boolean("q") == false);
// Test AnimState simple getters on wrong-type data.
LuaAssert(L, astate.get_string("half") == "");
LuaAssert(L, astate.get_xyz("half") == util::DXYZ(0,0,0));
LuaAssert(L, astate.get_number("color") == 0.0);
LuaAssert(L, astate.get_boolean("color") == false);
// Test AnimState persistence manipulation.
astate.set_persistent("color");
astate.set_persistent("nice");
LuaAssertStrEq(L, astate.debug_string(), "color=blue half:0.5 nice=true xyz:1,2,3");
// Test AnimState parser.
astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
// Test animstate encoding and decoding.
astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
enc = astate.encode();
astate.clear();
astate.decode(enc);
LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
astate.decode_persistent(enc);
LuaAssertStrEq(L, astate.debug_string(), "mean=true pos=3,4,5");
// Test AnimCoreState.decode
//
astate.clear_and_parse("color=blue xyz=1,2,3 plane=banana chicken=3");
core.decode(astate.encode());
LuaAssert(L, core.plane == "banana");
LuaAssert(L, core.xyz == util::DXYZ(1,2,3));
// Verify that a newly-constructed AnimQueue is in a reasonable default state.
//
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
// Clear an AnimQueue to a specified initial state.
//
astate.clear_and_parse("color=blue xyz=1,2,3 plane=somewhere");
aq.clear(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; color=blue plane=somewhere xyz=1,2,3");
// Add animation steps to animation queue.
// Note: each step is independent of the previous one, no composition is being done.
//
astate.clear_and_parse("xyz=1,2,3 plane=earth");
aq.clear(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3");
astate.clear_and_parse("xyz=4,5,6 action:jump");
aq.add(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6");
astate.clear_and_parse("plane=moon airline:southwest");
aq.add(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon");
astate.clear_and_parse("color=blue");
aq.add(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
// Try reducing the animation queue size limit.
//
aq.set_limit(2);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=2; airline:southwest plane=moon; color=blue");
// Test get_final_persistent, get_final_everything, get_final_core_state
//
astate.clear_and_parse("action:jump plane=earth xyz=1,2,3 bouncy:true");
aq.clear(astate);
astate = aq.get_final_persistent();
LuaAssertStrEq(L, astate.debug_string(), "plane=earth xyz=1,2,3");
astate = aq.get_final_everything();
LuaAssertStrEq(L, astate.debug_string(), "action:jump bouncy:true plane=earth xyz=1,2,3");
core = aq.get_final_core_state();
LuaAssert(L, core.plane == "earth");
LuaAssert(L, core.xyz == util::DXYZ(1,2,3));
// Serialize a queue.
//
aq.set_limit(10);
astate.clear_and_parse("xyz=1,2,3 plane=earth");
aq.clear(astate);
astate.clear_and_parse("xyz=4,5,6 action:jump");
aq.add(astate);
astate.clear_and_parse("plane=moon airline:southwest");
aq.add(astate);
astate.clear_and_parse("color=blue");
aq.add(astate);
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
aq.serialize(&sb);
// Deserialize a queue.
//
aqs.set_limit(7);
aqs.clear();
LuaAssert(L, !aqs.exactly_equal(aq));
aqs.deserialize(&sb);
LuaAssert(L, aqs.exactly_equal(aq));
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
// Test diff and patch.
//
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
aqs.set_limit(7);
aqs.clear();
sb.clear();
aqs.diff(aq, &sb);
//int difflen1 = sb.fill();
LuaAssert(L, !aqs.exactly_equal(aq));
aqs.patch(&sb, nullptr);
LuaAssert(L, aqs.exactly_equal(aq));
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
// Test that diff and patch are more efficient when the two queues contain some shared steps.
//
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
astate.clear_and_parse("xyz=4,5,6 action:jump");
aqs.clear(astate);
astate.clear_and_parse("plane=earth xyz=1,2,3");
aqs.add(astate);
astate.clear_and_parse("plane=moon airline:southwest");
aqs.add(astate);
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; action:jump xyz=4,5,6; plane=earth xyz=1,2,3; airline:southwest plane=moon");
sb.clear();
aqs.diff(aq, &sb);
//int difflen2 = sb.fill();
LuaAssert(L, !aqs.exactly_equal(aq));
aqs.patch(&sb, nullptr);
LuaAssert(L, aqs.exactly_equal(aq));
LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
// TODO: if we make the diff routine more efficient, this should be true.
// LuaAssert(L, difflen2 < (difflen1 / 2));
return 0;
}