diff --git a/luprex/cpp/core/animqueue.cpp b/luprex/cpp/core/animqueue.cpp index 27701058..10c7bb42 100644 --- a/luprex/cpp/core/animqueue.cpp +++ b/luprex/cpp/core/animqueue.cpp @@ -21,7 +21,7 @@ static const char *vtname(AnimValueType vt) { } -uint64_t hash_encstep(uint64_t prev, std::string_view s) { +static uint64_t hash_encstep(uint64_t prev, std::string_view s) { return util::hash_string(util::HashValue(123, prev), s).first; } @@ -420,102 +420,114 @@ void AnimCoreState::decode(std::string_view s) { } int AnimQueue::get_size_limit() const { + if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); return sb.read_uint8(); } int AnimQueue::get_actual_size() const { + if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); sb.read_bytes(1); return sb.read_uint8(); } uint64_t AnimQueue::get_final_hash() const { + if (encqueue_ == nullptr) return 0; StreamBuffer sb(*encqueue_); sb.read_bytes(2); return sb.read_uint64(); } std::string_view AnimQueue::get_final_encstep() const { + if (encqueue_ == nullptr) return std::string_view(); StreamBuffer sb(*encqueue_); sb.read_bytes(10); return sb.read_string_view(); } -void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, bool keepold) { - // Make sure the size limit is reasonable. - assert((limit >= 2) && (limit <= 250)); +AnimQueue::QueueRange AnimQueue::get_range(int lo, int hi) { + // Clamp lo and hi to the valid range (0 to actual_size). + // + int actual_size = get_actual_size(); + if (lo < 0) lo = 0; + if (hi > actual_size) hi = actual_size; - // You must either add a new step or retain an old step. The queue can't be empty. - assert(keepold || add); + // Abort early if the range is empty. This avoids several edge cases. + // + if (lo >= hi) return QueueRange(0, std::string_view()); - // Find out how many old steps we'll be retaining, ignoring the size limit. - int nretain = 0; - if (keepold) nretain = get_actual_size(); - - // If retaining all steps would overflow the size limit, retain fewer. - int retain_limit = limit; - if (add) retain_limit -= 1; - if (nretain > retain_limit) nretain = retain_limit; - - // Calculate the new size of the queue. - int new_size = add ? (nretain + 1) : nretain; - - // If we're retaining steps, extract them from the old queue. - std::string_view retain; - if (nretain > 0) { - std::string_view oldqueue(*encqueue_); - StreamBuffer sb(oldqueue); - sb.read_bytes(2); // Skip over the header. - int pos1 = sb.total_reads(); - for (int i = 0; i < nretain; i++) { - sb.read_uint64(); - sb.read_string_view(); - } - int pos2 = sb.total_reads(); - retain = oldqueue.substr(pos1, pos2 - pos1); + // Get the entries. + // + std::string_view queueview(*encqueue_); + StreamBuffer sb(queueview); + sb.read_bytes(2); // Skip over the header. + for (int i = 0; i < lo; i++) { + sb.read_uint64(); + sb.read_string_view(); } - - // If we're adding a step, calculate its hash. - uint64_t add_hash = 0; - if (add) { - uint64_t prev_hash = 0; - if (nretain > 0) prev_hash = get_final_hash(); - add_hash = hash_encstep(prev_hash, add_enc); + int pos1 = sb.total_reads(); + for (int i = lo; i < hi; i++) { + sb.read_uint64(); + sb.read_string_view(); } + int pos2 = sb.total_reads(); + return QueueRange(hi-lo, queueview.substr(pos1, pos2 - pos1)); +} - // Finally, encode everything into a binary blob. +uint64_t AnimQueue::hash_encstep(const QueueRange &prev, std::string_view s) { + uint64_t prev_hash = 0; + if (prev.size > 0) { + StreamBuffer retsb(prev.entries); + prev_hash = retsb.read_uint64(); + } + return ::hash_encstep(prev_hash, s); +} + +void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi) { + // Get the retained entries. + QueueRange keeprange = get_range(keeplo, keephi); + + // Encode everything into a binary blob. StreamBuffer result; result.write_uint8(limit); - result.write_uint8(new_size); + result.write_uint8(keeprange.size + (add ? 1:0)); if (add) { + uint64_t add_hash = hash_encstep(keeprange, add_enc); result.write_uint64(add_hash); result.write_string(add_enc); } - result.write_bytes(retain); + result.write_bytes(keeprange.entries); // Replace the shared string. encqueue_ = std::make_shared(result.view()); } AnimQueue::AnimQueue() { - update_encqueue(10, true, AnimState().encode(), false); + update_encqueue(10, true, AnimState().encode(), 0, 0); } void AnimQueue::clear(const AnimState &state) { - update_encqueue(get_size_limit(), true, state.encode(), false); + update_encqueue(get_size_limit(), true, state.encode(), 0, 0); } void AnimQueue::clear() { - update_encqueue(get_size_limit(), true, AnimState().encode(), false); + update_encqueue(get_size_limit(), true, AnimState().encode(), 0, 0); } void AnimQueue::set_limit(int limit) { - update_encqueue(limit, false, "", true); + assert((limit >= 2) && (limit <= 250)); + update_encqueue(limit, false, std::string_view(), 0, limit); } void AnimQueue::add(const AnimState &state) { - update_encqueue(get_size_limit(), true, state.encode(), true); + int limit = get_size_limit(); + update_encqueue(limit, true, state.encode(), 0, limit - 1); +} + +void AnimQueue::replace(const AnimState &state) { + int limit = get_size_limit(); + update_encqueue(limit, true, state.encode(), 1, limit); } void AnimQueue::serialize(StreamBuffer *sb) const { diff --git a/luprex/cpp/core/animqueue.hpp b/luprex/cpp/core/animqueue.hpp index 9d4add91..14434337 100644 --- a/luprex/cpp/core/animqueue.hpp +++ b/luprex/cpp/core/animqueue.hpp @@ -268,6 +268,13 @@ public: // void add(const AnimState &state); + // Replace the most recent animation step. + // + // Note: replace does not automatically compose the step with the previous + // step, you have to do that yourself. + // + void replace(const AnimState &state); + // Serialize or deserialize to a StreamBuffer // void serialize(StreamBuffer *sb) const; @@ -316,14 +323,41 @@ public: util::SharedStdString get_encoded_queue() const { return encqueue_; } private: - // Update the encoded queue. - // - // You must specify the new size limit. - // You may optionally specify an encstep to add. - // If keepold, then old steps will be retained up to the size limit. - // - void update_encqueue(int limit, bool add, std::string_view add_enc, bool keepold); + struct QueueRange { + int size; + std::string_view entries; + QueueRange(int sz, std::string_view ent) : size(sz), entries(ent) {} + }; + // Get a range of entries from the queue. + // + // You must specify a range (lo-hi) of steps. In this numbering, 0 is the + // most recent entry in the queue. The indices lo and hi are automatically + // clamped to the valid range (0 to actual_size). If lo >= hi, then an + // empty range is returned. + // + QueueRange get_range(int lo, int hi); + + // Hash a new step given the range of steps that precede it. + // + static uint64_t hash_encstep(const QueueRange &prev, std::string_view step); + + // Update the animation queue. + // + // The range (keeplo to keephi) specifies which old steps should be + // retained. The numbers keephi and keeplo are automatically clamped + // so that they lie inside the actual size of the queue. + // + // If add is true, then an additional step is added to the queue. + // The hash of the new step is calculated automatically. + // + // There is no enforcement that you respected the size limit that you + // specified. For example, you could say "keep 0-5, and add 1." That + // would make 6 entries in the queue. It is up to the caller to respect + // the size limit. The value passed in is just for reporting. + // + void update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi); + // Read values from the header of the encqueue. // int get_size_limit() const; diff --git a/luprex/cpp/core/world-accessor.cpp b/luprex/cpp/core/world-accessor.cpp index 89e51a31..8fba0bda 100644 --- a/luprex/cpp/core/world-accessor.cpp +++ b/luprex/cpp/core/world-accessor.cpp @@ -45,85 +45,85 @@ LuaDefine(tangible_animdebug, "tan", return LS.result(); } -LuaDefine(tangible_animstate, "tan", - "|Return the animation state variables of the tangible as a table." +LuaDefine(tangible_animfinal, "tan", + "|Return the final step in the animation queue." "|" - "|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." + "|The animation queue stores animation steps. This function returns" + "|the final animation step. An animation step consists of key-value" + "|pairs. Some of those key-value pairs describe the last thing that" + "|happened to the tangible. Others describe the final resting place" + "|of the tangible." "|" + "|For example, if the tangible were a pirate chest, the key-value" + "|pairs might be:" + "|" + "| action='open' # last thing the chest did" "| 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." + "|See doc(tangible.animinit) for more information about animation queues." "|") { 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(); + AnimState state = tan->anim_queue_.get_final_everything(); state.to_lua(LS, result, true); return LS.result(); } LuaDefine(tangible_animinit, "tan,config", - "|Reinitialize the animation queue." + "|Reinitialize the animation queue and specify persistent state." "|" - "|The animation queue stores certain animation state variables." - "|See doc(tangible.animstate) for more information about animation" - "|state variables." + "|Every tangible has an animation queue. The queue consists of a" + "|sequence of animation steps. Each step consists of a list of" + "|key-value pairs. For example, if you want a human person to jump" + "|three feet in the air, you might find this animation step in the" + "|animation queue:" + "|" + "| action='jump' - the name of the animation to perform" + "| height=3.0 - the height to which you want him to jump" + "| xyz={1,2,3} - person's xyz coordinate during the jump" + "| plane=earth - plane where the jump takes place" + "|" + "|Some of those key-value pairs are 'persistent'. For example, xyz is" + "|persistent. That means that the player must always have an xyz" + "|coordinate. Every single animation step in the queue must" + "|contain a value for xyz. Likewise, 'plane' is a persistent variable." + "|The player must always be on some plane or another." + "|" + "|When you add an animation step to the animation queue, you do not have" + "|to always specify xyz and plane. For example, you can legally say:" + "|" + "| tangible.animate(a, nil, {action='jump', height=3.0}))" + "|" + "|This adds a step to the animation queue. That step contains" + "|xyz and plane, even though we didn't specify xyz and plane in" + "|the 'animate' command above. The values for xyz and plane will be" + "|copied over from the previous animation step. In this way, those values" + "|get persisted: they stay the same unless you change them in" + "|the 'animate' command." + "|" + "|There are five hardwired persistent variables: plane,xyz,facing,bp,model." + "|These five variables are persistent no matter what. This function," + "|tangible.animinit, optionally allows you to create more persistent" + "|variables. For example, let's say you have a pirate chest. You might" + "|want to add two persistent variables in addition to the usual set:" + "|" + "| open=true - whether the chest is open or closed" + "| heapsize=0.8 - the size of the heap of coins" + "|" + "|Making a variable persistent means that it will always have a value" + "|no matter what you do." "|" "|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." + "|persistent state variables that are retained by the tangible's" + "|animation queue. You must provide a table containing all the" + "|persistent values you want, and their initial values." "|") { LuaArg tanobj, config; LuaDefStack LS(L, tanobj, config); @@ -144,46 +144,65 @@ LuaDefine(tangible_animinit, "tan,config", return LS.result(); } - -LuaDefine(tangible_animate, "tan,config", +LuaDefine(tangible_animate, "tan,options,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)." + "|It might be useful to read doc(tangible.animinit) before reading" + "|more." "|" "|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" + "|Some of the key-value pairs may match the name of a persistent" "|variable. If so, that key-value pair permanently changes the" - "|value of that animation state variable. The new value will" + "|value of that persistent 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" + "|Some of the key-value pairs may not match the name of any persistent" + "|variable. If so, that key-value pair is part of the" "|animation step, but nothing is propagated forward to future animation" "|steps." "|" + "|The options can be nil, or options can be a table containing" + "|the following flags:" + "|" + "| replace: if true, then the last step in the queue is removed," + "| and the new animation replaces it. Persistent state is carried" + "| over from the step that was replaced." + "|" "|") { - LuaArg tanobj, config; - LuaDefStack LS(L, tanobj, config); + LuaArg tanobj, options, steptab; + LuaVar option; + LuaDefStack LS(L, option, tanobj, options, steptab); + bool replace = false; + if (!LS.isnil(options)) { + LuaKeywordParser kp(LS, options); + if (kp.parse(option, "replace")) { + replace = LS.ckboolean(option); + } + } 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); + eng::string error = state.apply_lua(LS, steptab, false); if (!error.empty()) { luaL_error(L, "%s", error.c_str()); } - tan->anim_queue_.add(state); + if (replace) { + tan->anim_queue_.replace(state); + } else { + tan->anim_queue_.add(state); + } tan->update_plane_item(); return LS.result(); } + LuaDefine(tangible_setclass, "tan,class", "|Set the class of the tangible." "|The class can be a 'class table' (ie, a table of methods), "