diff --git a/doc/code/curves.md b/doc/code/curves.md index 376c4c938c..1a4c7f6b4a 100644 --- a/doc/code/curves.md +++ b/doc/code/curves.md @@ -17,6 +17,7 @@ Curves are an integral part of openage's event-based game simulation. 2. [Container](#container) 1. [Queue](#queue) 2. [Unordered Map](#unordered-map) +4. [Compression](#compression) ## Motivation @@ -132,6 +133,9 @@ Modify operations insert values for a specific point in time. | `set_insert(t, value)` | Insert a new keyframe value at time `t` | | `set_last(t, value)` | Insert a new keyframe value at time `t`; delete all keyframes after time `t` | | `set_replace(t, value)` | Insert a new keyframe value at time `t`; remove all other keyframes with time `t` | +| `compress(t)` | Remove redundant keyframes at and after time `t`; see [Compression] for more info | + +[Compression]: #compression **Copy** @@ -253,3 +257,27 @@ Unordered map curve containers store key-value pairs while additionally keeping track of element insertion time. Requests for a key `k` at time `t` will return the value of `k` at that time. The unordered map can also be iterated over for a specific time `t` which allows access to all key-value pairs that were in the map at time `t`. + +## Compression + +Curves support basic lossless compression by removing redundant keyframes from the curve. +Keyframes are considered redundant if they do not change any interpolation results, i.e. +the result of `get(t)` does not change. + +The most straight-forward way to use compression with primitive curves is the `compress(t)` +method. `compress(t)` iterates over the curve and removes all redundant keyframes after +or at time `t`. The runtime has linear complexity `O(n)` based on the number of elements +in the keyframe container. + +Furthermore, primitive curves support incremental compression during insertion for the +`set_insert(t, value)` and `set_last(t, value)` methods via their `compress` argument. +If compression is active, `(t, value)` is only inserted when it is not a redundant +keyframe. `sync(Curve, t)` also supports compression with a flag `compress` passed as +an argument. + +Compression may be used in cases where the size should be kept small, e.g. when the curve +is tranferred via network or recorded in a replay file. Another application of compression +is in the [renderer](/doc/code/renderer/README.md) for the discrete curves storing an object's +animations. Since compression removes redundant animation entries, the renderer can determine +when the current animation has started much easier as this is then returned by the keyframe +time in `frame(t)`. diff --git a/doc/code/game_simulation/activity.md b/doc/code/game_simulation/activity.md index 0b84864fe9..edd623f419 100644 --- a/doc/code/game_simulation/activity.md +++ b/doc/code/game_simulation/activity.md @@ -48,11 +48,12 @@ you can use available [BPMN tools](https://bpmn.io/) to draw activity node graph ## Node Types -| Type | Inputs | Outputs | Description | -| ---------------- | ------ | ------- | ------------------------- | -| `START` | 0 | 1 | Start of activity | -| `END` | 1 | 0 | End of activity | -| `TASK_SYSTEM` | 1 | 1 | Run built-in system | -| `TASK_CUSTOM` | 1 | 1 | Run custom function | -| `XOR_EVENT_GATE` | 1 | 1+ | Wait for event and branch | -| `XOR_GATE` | 1 | 1+ | Branch on condition | +| Type | Inputs | Outputs | Description | +| ----------------- | ------ | ------- | ------------------------- | +| `START` | 0 | 1 | Start of activity | +| `END` | 1 | 0 | End of activity | +| `TASK_SYSTEM` | 1 | 1 | Run built-in system | +| `TASK_CUSTOM` | 1 | 1 | Run custom function | +| `XOR_EVENT_GATE` | 1 | 1+ | Wait for event and branch | +| `XOR_GATE` | 1 | 1+ | Branch on condition | +| `XOR_SWITCH_GATE` | 1 | 1+ | Branch on value | diff --git a/libopenage/curve/CMakeLists.txt b/libopenage/curve/CMakeLists.txt index 1aa1efb55b..60680c1dac 100644 --- a/libopenage/curve/CMakeLists.txt +++ b/libopenage/curve/CMakeLists.txt @@ -1,5 +1,6 @@ add_sources(libopenage base_curve.cpp + concept.cpp continuous.cpp discrete.cpp discrete_mod.cpp diff --git a/libopenage/curve/base_curve.h b/libopenage/curve/base_curve.h index dc58885b98..123a67c88d 100644 --- a/libopenage/curve/base_curve.h +++ b/libopenage/curve/base_curve.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include #include @@ -13,6 +14,7 @@ #include "log/log.h" #include "log/message.h" +#include "curve/concept.h" #include "curve/keyframe_container.h" #include "event/evententity.h" #include "time/time.h" @@ -26,7 +28,7 @@ class EventLoop; namespace curve { -template +template class BaseCurve : public event::EventEntity { public: BaseCurve(const std::shared_ptr &loop, @@ -73,30 +75,62 @@ class BaseCurve : public event::EventEntity { /** * Insert/overwrite given value at given time and erase all elements * that follow at a later time. + * * If multiple elements exist at the given time, * overwrite the last one. + * + * @param at Time the keyframe is inserted at. + * @param value Value of the keyframe. + * @param compress If true, only insert the keyframe if the value at time \p at + * is different from the given value. */ - virtual void set_last(const time::time_t &at, const T &value); + virtual void set_last(const time::time_t &at, + const T &value, + bool compress = false); /** * Insert a value at the given time. + * * If there already is a value at this time, * the value is inserted directly after the existing one. + * + * @param at Time the keyframe is inserted at. + * @param value Value of the keyframe. + * @param compress If true, only insert the keyframe if the value at time \p at + * is different from the given value. */ - virtual void set_insert(const time::time_t &at, const T &value); + virtual void set_insert(const time::time_t &at, + const T &value, + bool compress = false); /** * Insert a value at the given time. + * * If there already is a value at this time, * the given value will replace the first value with the same time. + * + * @param at Time the keyframe is inserted at. + * @param value Value of the keyframe. */ - virtual void set_replace(const time::time_t &at, const T &value); + virtual void set_replace(const time::time_t &at, + const T &value); /** * Remove all values that have the given time. */ virtual void erase(const time::time_t &at); + /** + * Compress the curve by removing redundant keyframes. + * + * A keyframe is redundant if it doesn't change the value calculation of the curve + * at any given time, e.g. duplicate keyframes. + * + * @param start Start time at which keyframes are compressed (default = -INF). + * Using the default value compresses ALL keyframes of the curve. + */ + virtual void compress(const time::time_t &start = time::TIME_MIN) = 0; + /** * Integrity check, for debugging/testing reasons only. */ @@ -112,9 +146,13 @@ class BaseCurve : public event::EventEntity { * @param start Start time at which keyframes are replaced (default = -INF). * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. + * @param compress If true, redundant keyframes are not copied during the sync. + * Redundant keyframes are keyframes that don't change the value + * calculaton of the curve at any given time, e.g. duplicate keyframes. */ void sync(const BaseCurve &other, - const time::time_t &start = time::TIME_MIN); + const time::time_t &start = time::TIME_MIN, + bool compress = false); /** * Copy keyframes from another curve (with a different element type) to this curve. @@ -129,11 +167,15 @@ class BaseCurve : public event::EventEntity { * @param start Start time at which keyframes are replaced (default = -INF). * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. + * @param compress If true, redundant keyframes are not copied during the sync. + * Redundant keyframes are keyframes that don't change the value + * calculaton of the curve at any given time, e.g. duplicate keyframes. */ - template + template void sync(const BaseCurve &other, const std::function &converter, - const time::time_t &start = time::TIME_MIN); + const time::time_t &start = time::TIME_MIN, + bool compress = false); /** * Get the identifier of this curve. @@ -198,8 +240,10 @@ class BaseCurve : public event::EventEntity { }; -template -void BaseCurve::set_last(const time::time_t &at, const T &value) { +template +void BaseCurve::set_last(const time::time_t &at, + const T &value, + bool compress) { auto hint = this->container.last(at, this->last_element); // erase max one same-time value @@ -209,6 +253,13 @@ void BaseCurve::set_last(const time::time_t &at, const T &value) { hint = this->container.erase_after(hint); + if (compress and this->get(at) == value) { + // skip insertion if the value is the same as the last one + // erasure still happened, so we need to notify about the change + this->changes(at); + return; + } + this->container.insert_before(at, value, hint); this->last_element = hint; @@ -216,32 +267,42 @@ void BaseCurve::set_last(const time::time_t &at, const T &value) { } -template -void BaseCurve::set_insert(const time::time_t &at, const T &value) { +template +void BaseCurve::set_insert(const time::time_t &at, + const T &value, + bool compress) { + if (compress and this->get(at) == value) { + // skip insertion if the value is the same as the last one + return; + } + auto hint = this->container.insert_after(at, value, this->last_element); + // check if this is now the final keyframe if (this->container.get(hint).time() > this->container.get(this->last_element).time()) { this->last_element = hint; } + this->changes(at); } -template -void BaseCurve::set_replace(const time::time_t &at, const T &value) { +template +void BaseCurve::set_replace(const time::time_t &at, + const T &value) { this->container.insert_overwrite(at, value, this->last_element); this->changes(at); } -template +template void BaseCurve::erase(const time::time_t &at) { this->last_element = this->container.erase(at, this->last_element); this->changes(at); } -template +template std::pair BaseCurve::frame(const time::time_t &time) const { auto e = this->container.last(time, this->container.size()); auto elem = this->container.get(e); @@ -249,7 +310,7 @@ std::pair BaseCurve::frame(const time::time_t &time) c } -template +template std::pair BaseCurve::next_frame(const time::time_t &time) const { auto e = this->container.last(time, this->container.size()); e++; @@ -257,7 +318,7 @@ std::pair BaseCurve::next_frame(const time::time_t &ti return std::make_pair(elem.time(), elem.val()); } -template +template std::string BaseCurve::str() const { std::stringstream ss; ss << "Curve[" << this->idstr() << "]{" << std::endl; @@ -269,7 +330,7 @@ std::string BaseCurve::str() const { return ss.str(); } -template +template void BaseCurve::check_integrity() const { time::time_t last_time = time::TIME_MIN; for (const auto &keyframe : this->container) { @@ -280,9 +341,10 @@ void BaseCurve::check_integrity() const { } } -template +template void BaseCurve::sync(const BaseCurve &other, - const time::time_t &start) { + const time::time_t &start, + bool compress) { // Copy keyframes between containers for t >= start this->last_element = this->container.sync(other.container, start); @@ -293,15 +355,20 @@ void BaseCurve::sync(const BaseCurve &other, this->set_insert(start, get_other); } + if (compress) { + this->compress(start); + } + this->changes(start); } -template -template +template +template void BaseCurve::sync(const BaseCurve &other, const std::function &converter, - const time::time_t &start) { + const time::time_t &start, + bool compress) { // Copy keyframes between containers for t >= start this->last_element = this->container.sync(other.get_container(), converter, start); @@ -312,6 +379,10 @@ void BaseCurve::sync(const BaseCurve &other, this->set_insert(start, get_other); } + if (compress) { + this->compress(start); + } + this->changes(start); } diff --git a/libopenage/curve/concept.cpp b/libopenage/curve/concept.cpp new file mode 100644 index 0000000000..aa1b8c4612 --- /dev/null +++ b/libopenage/curve/concept.cpp @@ -0,0 +1,9 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "concept.h" + + +namespace openage::curve { + + +} // namespace openage::curve diff --git a/libopenage/curve/concept.h b/libopenage/curve/concept.h new file mode 100644 index 0000000000..8d05df948a --- /dev/null +++ b/libopenage/curve/concept.h @@ -0,0 +1,15 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +namespace openage::curve { + +/** + * Concept for keyframe values. + */ +template +concept KeyframeValueLike = std::copyable && std::equality_comparable; + +} // namespace openage::curve diff --git a/libopenage/curve/continuous.h b/libopenage/curve/continuous.h index 0cf438f237..9f61bb166c 100644 --- a/libopenage/curve/continuous.h +++ b/libopenage/curve/continuous.h @@ -5,6 +5,7 @@ #include #include +#include "curve/concept.h" #include "curve/interpolated.h" #include "time/time.h" @@ -22,7 +23,7 @@ namespace openage::curve { * The bound template type T has to implement `operator+(T)` and * `operator*(time::time_t)`. */ -template +template class Continuous : public Interpolated { public: using Interpolated::Interpolated; @@ -33,18 +34,24 @@ class Continuous : public Interpolated { * If multiple elements exist at the given time, * overwrite all of them. */ - void set_last(const time::time_t &t, const T &value) override; + void set_last(const time::time_t &t, + const T &value, + bool compress = false) override; /** This just calls set_replace in order to guarantee the continuity. */ - void set_insert(const time::time_t &t, const T &value) override; + void set_insert(const time::time_t &t, + const T &value, + bool compress = false) override; /** human readable identifier */ std::string idstr() const override; }; -template -void Continuous::set_last(const time::time_t &at, const T &value) { +template +void Continuous::set_last(const time::time_t &at, + const T &value, + bool compress) { auto hint = this->container.last(at, this->last_element); // erase all same-time entries @@ -54,6 +61,13 @@ void Continuous::set_last(const time::time_t &at, const T &value) { hint = this->container.erase_after(hint); + if (compress and this->get(at) == value) { + // skip insertion if the value is the same as the last one + // erasure still happened, so we need to notify about the change + this->changes(at); + return; + } + this->container.insert_before(at, value, hint); this->last_element = hint; @@ -61,13 +75,15 @@ void Continuous::set_last(const time::time_t &at, const T &value) { } -template -void Continuous::set_insert(const time::time_t &t, const T &value) { +template +void Continuous::set_insert(const time::time_t &t, + const T &value, + bool /* compress */) { this->set_replace(t, value); } -template +template std::string Continuous::idstr() const { std::stringstream ss; ss << "ContinuousCurve["; diff --git a/libopenage/curve/discrete.h b/libopenage/curve/discrete.h index 44fe132de6..a7af01f1b6 100644 --- a/libopenage/curve/discrete.h +++ b/libopenage/curve/discrete.h @@ -9,6 +9,7 @@ #include #include "curve/base_curve.h" +#include "curve/concept.h" #include "time/time.h" @@ -18,13 +19,8 @@ namespace openage::curve { * Does not interpolate between values. The template type does only need to * implement `operator=` and copy ctor. */ -template +template class Discrete : public BaseCurve { - static_assert(std::is_copy_assignable::value, - "Template type is not copy assignable"); - static_assert(std::is_copy_constructible::value, - "Template type is not copy constructible"); - public: using BaseCurve::BaseCurve; @@ -34,6 +30,8 @@ class Discrete : public BaseCurve { */ T get(const time::time_t &t) const override; + void compress(const time::time_t &start = time::TIME_MIN) override; + /** * Get a human readable id string. */ @@ -51,15 +49,42 @@ class Discrete : public BaseCurve { }; -template +template T Discrete::get(const time::time_t &time) const { auto e = this->container.last(time, this->last_element); this->last_element = e; // TODO if Caching? return this->container.get(e).val(); } +template +void Discrete::compress(const time::time_t &start) { + auto e = this->container.last_before(start, this->last_element); + + // Store elements that should be kept + std::vector> to_keep; + auto last_kept = e; + for (auto current = e + 1; current < this->container.size(); ++current) { + if (this->container.get(last_kept).val() != this->container.get(current).val()) { + // Keep values that are different from the last kept value + to_keep.push_back(this->container.get(current)); + last_kept = current; + } + } + + // Erase all elements and insert the kept ones + this->container.erase_after(e); + for (auto &elem : to_keep) { + this->container.insert_after(elem, this->container.size() - 1); + } + + // Update the cached element pointer + this->last_element = e; + + // Notify observers about the changes + this->changes(start); +} -template +template std::string Discrete::idstr() const { std::stringstream ss; ss << "DiscreteCurve["; @@ -74,7 +99,7 @@ std::string Discrete::idstr() const { } -template +template std::pair Discrete::get_time(const time::time_t &time) const { auto e = this->container.last(time, this->last_element); this->last_element = e; @@ -84,7 +109,7 @@ std::pair Discrete::get_time(const time::time_t &time) const } -template +template std::optional> Discrete::get_previous(const time::time_t &time) const { auto e = this->container.last(time, this->last_element); this->last_element = e; diff --git a/libopenage/curve/discrete_mod.h b/libopenage/curve/discrete_mod.h index 953939f975..33adcbbccc 100644 --- a/libopenage/curve/discrete_mod.h +++ b/libopenage/curve/discrete_mod.h @@ -9,6 +9,7 @@ #include #include "curve/base_curve.h" +#include "curve/concept.h" #include "curve/discrete.h" #include "time/time.h" #include "util/fixed_point.h" @@ -27,20 +28,19 @@ namespace openage::curve { * always be inserted at t = 0. Also, the last keyframe should have the same value * as the first keyframe as a convention. */ -template +template class DiscreteMod : public Discrete { - static_assert(std::is_copy_assignable::value, - "Template type is not copy assignable"); - static_assert(std::is_copy_constructible::value, - "Template type is not copy constructible"); - public: using Discrete::Discrete; // Override insertion/erasure to get interval time - void set_last(const time::time_t &at, const T &value) override; - void set_insert(const time::time_t &at, const T &value) override; + void set_last(const time::time_t &at, + const T &value, + bool compress = false) override; + void set_insert(const time::time_t &at, + const T &value, + bool compress = false) override; void erase(const time::time_t &at) override; /** @@ -71,16 +71,20 @@ class DiscreteMod : public Discrete { }; -template -void DiscreteMod::set_last(const time::time_t &at, const T &value) { - BaseCurve::set_last(at, value); +template +void DiscreteMod::set_last(const time::time_t &at, + const T &value, + bool compress) { + BaseCurve::set_last(at, value, compress); this->time_length = at; } -template -void DiscreteMod::set_insert(const time::time_t &at, const T &value) { - BaseCurve::set_insert(at, value); +template +void DiscreteMod::set_insert(const time::time_t &at, + const T &value, + bool compress) { + BaseCurve::set_insert(at, value, compress); if (this->time_length < at) { this->time_length = at; @@ -88,7 +92,7 @@ void DiscreteMod::set_insert(const time::time_t &at, const T &value) { } -template +template void DiscreteMod::erase(const time::time_t &at) { BaseCurve::erase(at); @@ -98,7 +102,7 @@ void DiscreteMod::erase(const time::time_t &at) { } -template +template std::string DiscreteMod::idstr() const { std::stringstream ss; ss << "DiscreteRingCurve["; @@ -113,7 +117,7 @@ std::string DiscreteMod::idstr() const { } -template +template T DiscreteMod::get_mod(const time::time_t &time, const time::time_t &start) const { time::time_t offset = time - start; if (this->time_length == 0) { @@ -126,8 +130,9 @@ T DiscreteMod::get_mod(const time::time_t &time, const time::time_t &start) c } -template -std::pair DiscreteMod::get_time_mod(const time::time_t &time, const time::time_t &start) const { +template +std::pair DiscreteMod::get_time_mod(const time::time_t &time, + const time::time_t &start) const { time::time_t offset = time - start; if (this->time_length == 0) { // modulo would fail here so return early @@ -139,8 +144,9 @@ std::pair DiscreteMod::get_time_mod(const time::time_t &time } -template -std::optional> DiscreteMod::get_previous_mod(const time::time_t &time, const time::time_t &start) const { +template +std::optional> DiscreteMod::get_previous_mod(const time::time_t &time, + const time::time_t &start) const { time::time_t offset = time - start; if (this->time_length == 0) { // modulo would fail here so return early diff --git a/libopenage/curve/interpolated.h b/libopenage/curve/interpolated.h index 564ba1d0af..89a0d6b2c9 100644 --- a/libopenage/curve/interpolated.h +++ b/libopenage/curve/interpolated.h @@ -3,6 +3,7 @@ #pragma once #include "curve/base_curve.h" +#include "curve/concept.h" #include "time/time.h" #include "util/fixed_point.h" @@ -17,7 +18,7 @@ namespace openage::curve { * The bound template type T has to implement `operator +(T)` and * `operator *(time::time_t)`. */ -template +template class Interpolated : public BaseCurve { public: using BaseCurve::BaseCurve; @@ -33,12 +34,30 @@ class Interpolated : public BaseCurve { * example for a <= t <= b: * val([a:x, b:y], t) = x + (y - x)/(b - a) * (t - a) */ - T get(const time::time_t &) const override; + + void compress(const time::time_t &start = time::TIME_MIN) override; + +private: + /** + * Get an interpolated value between two keyframes. + * + * 'before' and 'after' must be ordered such that the index of 'before' is + * less than the index of 'after'. + * + * @param before Index of the earlier keyframe. + * @param after Index of the later keyframe. + * @param elapsed Elapsed time after the earlier keyframe. + * + * @return Interpolated value. + */ + T interpolate(typename KeyframeContainer::elem_ptr before, + typename KeyframeContainer::elem_ptr after, + double elapsed) const; }; -template +template T Interpolated::get(const time::time_t &time) const { const auto e = this->container.last(time, this->last_element); this->last_element = e; @@ -59,8 +78,8 @@ T Interpolated::get(const time::time_t &time) const { // If the next element is at the same time, just return the value of this one. if (nxt == this->container.size() // use the last curve value - || offset == 0 // values equal -> don't need to interpolate - || interval == 0) { // values at the same time -> division-by-zero-error + || offset == 0 // values equal -> don't need to interpolate + || interval == 0) { // values at the same time -> division-by-zero-error return this->container.get(e).val(); } @@ -69,12 +88,61 @@ T Interpolated::get(const time::time_t &time) const { // TODO: Elapsed time does not use fixed point arithmetic double elapsed_frac = offset.to_double() / interval.to_double(); - // TODO: nxt->value - e->value will produce wrong results if - // the nxt->value < e->value and curve element type is unsigned - // Example: nxt = 2, e = 4; type = uint8_t ==> 2 - 4 = 254 - auto diff_value = (this->container.get(nxt).val() - this->container.get(e).val()) * elapsed_frac; - return this->container.get(e).val() + diff_value; + return this->interpolate(e, nxt, elapsed_frac); + } +} + +template +void Interpolated::compress(const time::time_t &start) { + // Find the last element before the start time + auto e = this->container.last_before(start, this->last_element); + + // Store elements that should be kept + std::vector> to_keep; + auto last_kept = e; + for (auto current = e + 1; current < this->container.size() - 1; ++current) { + // offset is between current keyframe and the last kept keyframe + auto offset = this->container.get(current).time() - this->container.get(last_kept).time(); + auto interval = this->container.get(current + 1).time() - this->container.get(last_kept).time(); + auto elapsed_frac = offset.to_double() / interval.to_double(); + + // Interpolate the value that would be at the current keyframe (if it didn't exist) + auto interpolated = this->interpolate(last_kept, current + 1, elapsed_frac); + if (interpolated != this->container.get(current).val()) { + // Keep values that are different from the interpolated value + to_keep.push_back(this->container.get(current)); + last_kept = current; + } } + // The last element is always kept, so we have to add it manually to keep it + to_keep.push_back(this->container.get(this->container.size() - 1)); + + // Erase all old keyframes after start and reinsert the non-redundant keyframes + this->container.erase_after(e); + for (auto elem : to_keep) { + this->container.insert_after(elem, this->container.size() - 1); + } + + // Update the cached element pointer + this->last_element = e; + + // Notify observers about the changes + this->changes(start); +} + +template +inline T Interpolated::interpolate(typename KeyframeContainer::elem_ptr before, + typename KeyframeContainer::elem_ptr after, + double elapsed) const { + ENSURE(before <= after, "Index of 'before' must be before 'after'"); + ENSURE(elapsed <= (this->container.get(after).time().to_double() + - this->container.get(before).time().to_double()), + "Elapsed time must be less than or equal to the time between before and after"); + // TODO: after->value - before->value will produce wrong results if + // after->value < before->value and curve element type is unsigned + // Example: after = 2, before = 4; type = uint8_t ==> 2 - 4 = 254 + auto diff_value = (this->container.get(after).val() - this->container.get(before).val()) * elapsed; + return this->container.get(before).val() + diff_value; } diff --git a/libopenage/curve/iterator.h b/libopenage/curve/iterator.h index d0346e1b5d..dd7c5f29eb 100644 --- a/libopenage/curve/iterator.h +++ b/libopenage/curve/iterator.h @@ -2,6 +2,7 @@ #pragma once +#include "curve/concept.h" #include "time/time.h" #include "util/fixed_point.h" @@ -11,7 +12,7 @@ namespace openage::curve { /** * Default interface for curve containers */ -template class CurveIterator { diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index e868783cbb..c8178ed339 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -2,6 +2,7 @@ #pragma once +#include "curve/concept.h" #include "time/time.h" #include "util/fixed_point.h" @@ -15,7 +16,7 @@ namespace openage::curve { * If you change this class, remember to update the gdb pretty printers * in etc/gdb_pretty/printers.py. */ -template +template class Keyframe { public: /** diff --git a/libopenage/curve/keyframe_container.h b/libopenage/curve/keyframe_container.h index d9bb9a0bbd..a69a2c9616 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -7,6 +7,7 @@ #include #include +#include "curve/concept.h" #include "curve/keyframe.h" #include "time/time.h" #include "util/fixed_point.h" @@ -23,7 +24,7 @@ namespace openage::curve { * the exact timestamp has to be known, it will always return the one closest, * less or equal to the requested one. **/ -template +template class KeyframeContainer { public: /** @@ -115,60 +116,111 @@ class KeyframeContainer { } /** - * Insert a new element without a hint. + * Insert a new element without submitting a hint. The search is + * started from the end of the data. * - * Starts the search for insertion at the end of the data. - * This function is not recommended for use, whenever possible, keep a hint - * to insert the data. + * The use of this function is discouraged, use it only, if your really + * do not have the possibility to get a hint. + * + * If there is a keyframe with identical time, this will + * insert the new keyframe before the old one. + * + * @param keyframe Keyframe to insert. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_before(const keyframe_t &value) { - return this->insert_before(value, this->container.size()); + elem_ptr insert_before(const keyframe_t &keyframe) { + return this->insert_before(keyframe, this->container.size()); } /** - * Insert a new element. The hint shall give an approximate location, where + * Insert a new element. + * + * The hint shall give an approximate location, where * the inserter will start to look for a insertion point. If a good hint is * given, the runtime of this function will not be affected by the current - * history size. If there is a keyframe with identical time, this will + * history size. + * + * If there is a keyframe with identical time, this will * insert the new keyframe before the old one. + * + * @param keyframe Keyframe to insert. + * @param hint Index of the approximate insertion location. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_before(const keyframe_t &value, const elem_ptr &hint); + elem_ptr insert_before(const keyframe_t &keyframe, + const elem_ptr &hint); /** - * Create and insert a new element without submitting a hint. The search is - * started from the end of the data. The use of this function is - * discouraged, use it only, if your really do not have the possibility to - * get a hint. + * Create and insert a new element without submitting a hint. The search + * is started from the end of the data. + * + * The use of this function is discouraged, use it only, if your really + * do not have the possibility to get a hint. + * + * If there is a keyframe with identical time, this will + * insert the new keyframe before the old one. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_before(const time::time_t &time, const T &value) { - return this->insert_before(keyframe_t(time, value), this->container.size()); + elem_ptr insert_before(const time::time_t &time, + const T &value) { + return this->insert_before(keyframe_t(time, value), + this->container.size()); } /** * Create and insert a new element. The hint gives an approximate location. + * * If there is a value with identical time, this will insert the new value * before the old one. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * @param hint Index of the approximate insertion location. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_before(const time::time_t &time, const T &value, const elem_ptr &hint) { + elem_ptr insert_before(const time::time_t &time, + const T &value, + const elem_ptr &hint) { return this->insert_before(keyframe_t(time, value), hint); } /** * Insert a new element, overwriting elements that have a - * time conflict. Give an approximate insertion location to minimize runtime + * time conflict. The hint gives an approximate insertion location to minimize runtime * on big-history curves. * `overwrite_all` == true -> overwrite all same-time elements. * `overwrite_all` == false -> overwrite the last of the time-conflict elements. + * + * @param keyframe Keyframe to insert. + * @param hint Index of the approximate insertion location. + * @param overwrite_all If true, overwrite all elements with the same time. + * If false, overwrite only the last element with the same time. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_overwrite(const keyframe_t &value, + elem_ptr insert_overwrite(const keyframe_t &keyframe, const elem_ptr &hint, bool overwrite_all = false); /** - * Insert a new value at given time which will overwrite the last of the + * Create and insert a new value at given time which will overwrite the last of the * elements with the same time. This function will start to search the time - * from the end of the data. The use of this function is discouraged, use it - * only, if your really do not have the possibility to get a hint. + * from the end of the data. + * + * The use of this function is discouraged, use itonly, if your really + * do not have the possibility to get a hint. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * + * @return The location (index) of the inserted element. */ elem_ptr insert_overwrite(const time::time_t &time, const T &value) { return this->insert_overwrite(keyframe_t(time, value), @@ -181,6 +233,14 @@ class KeyframeContainer { * element. If `overwrite_all` is true, overwrite all elements with same-time. * Provide a insertion hint to abbreviate the search for the * insertion point. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * @param hint Index of the approximate insertion location. + * @param overwrite_all If true, overwrite all elements with the same time. + * If false, overwrite only the last element with the same time. + * + * @return The location (index) of the inserted element. */ elem_ptr insert_overwrite(const time::time_t &time, const T &value, @@ -191,18 +251,32 @@ class KeyframeContainer { /** * Insert a new element, after a previous element when there's a time - * conflict. Give an approximate insertion location to minimize runtime on + * conflict. The hint gives an approximate insertion location to minimize runtime on * big-history curves. + * + * @param keyframe Keyframe to insert. + * @param hint Index of the approximate insertion location. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_after(const keyframe_t &value, const elem_ptr &hint); + elem_ptr insert_after(const keyframe_t &keyframe, + const elem_ptr &hint); /** - * Insert a new value at given time which will be prepended to the block of + * Create and insert a new value at given time which will be prepended to the block of * elements that have the same time. This function will start to search the - * time from the end of the data. The use of this function is discouraged, - * use it only, if your really do not have the possibility to get a hint. + * time from the end of the data. + * + * The use of this function is discouraged, use it only, if your really + * do not have the possibility to get a hint. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_after(const time::time_t &time, const T &value) { + elem_ptr insert_after(const time::time_t &time, + const T &value) { return this->insert_after(keyframe_t(time, value), this->container.size()); } @@ -210,8 +284,16 @@ class KeyframeContainer { /** * Create and insert a new element, which is added after a previous element with * identical time. Provide a insertion hint to abbreviate the search for the insertion point. + * + * @param time Time of the new keyframe. + * @param value Value of the new keyframe. + * @param hint Index of the approximate insertion location. + * + * @return The location (index) of the inserted element. */ - elem_ptr insert_after(const time::time_t &time, const T &value, const elem_ptr &hint) { + elem_ptr insert_after(const time::time_t &time, + const T &value, + const elem_ptr &hint) { return this->insert_after(keyframe_t(time, value), hint); } @@ -279,8 +361,6 @@ class KeyframeContainer { * Replaces all keyframes beginning at t >= start with keyframes from \p other. * * @param other Curve that keyframes are copied from. - * @param converter Function that converts the value type of \p other to the - * value type of \p this. * @param start Start time at which keyframes are replaced (default = -INF). * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. @@ -300,7 +380,7 @@ class KeyframeContainer { * Using the default value replaces ALL keyframes of \p this with * the keyframes of \p other. */ - template + template elem_ptr sync(const KeyframeContainer &other, const std::function &converter, const time::time_t &start = time::TIME_MIN); @@ -329,7 +409,7 @@ class KeyframeContainer { }; -template +template KeyframeContainer::KeyframeContainer() { // Create a default element at -Inf, that can always be dereferenced - so // there will by definition never be a element that cannot be dereferenced @@ -337,7 +417,7 @@ KeyframeContainer::KeyframeContainer() { } -template +template KeyframeContainer::KeyframeContainer(const T &defaultval) { // Create a default element at -Inf, that can always be dereferenced - so // there will by definition never be a element that cannot be dereferenced @@ -345,7 +425,7 @@ KeyframeContainer::KeyframeContainer(const T &defaultval) { } -template +template size_t KeyframeContainer::size() const { return this->container.size(); } @@ -359,7 +439,7 @@ size_t KeyframeContainer::size() const { * Intuitively, this function returns the element that set the last value * that determines the curve value for a searched time. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::last(const time::time_t &time, const KeyframeContainer::elem_ptr &hint) const { @@ -392,8 +472,10 @@ KeyframeContainer::last(const time::time_t &time, * * Intuitively, this function returns the element that comes right before the * first element that matches the search time. + * + * ASDF: Remove all comments for the implementations. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::last_before(const time::time_t &time, const KeyframeContainer::elem_ptr &hint) const { @@ -422,7 +504,7 @@ KeyframeContainer::last_before(const time::time_t &time, /* * Determine where to insert based on time, and insert. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, const KeyframeContainer::elem_ptr &hint) { @@ -448,7 +530,7 @@ KeyframeContainer::insert_before(const KeyframeContainer::keyframe_t &e, /* * Determine where to insert based on time, and insert, overwriting value(s) with same time. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::insert_overwrite(const KeyframeContainer::keyframe_t &e, const KeyframeContainer::elem_ptr &hint, @@ -478,7 +560,7 @@ KeyframeContainer::insert_overwrite(const KeyframeContainer::keyframe_t &e * Determine where to insert based on time, and insert. * If there is a time conflict, insert after the existing element. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::insert_after(const KeyframeContainer::keyframe_t &e, const KeyframeContainer::elem_ptr &hint) { @@ -497,7 +579,7 @@ KeyframeContainer::insert_after(const KeyframeContainer::keyframe_t &e, /* * Go from the end to the last_valid element, and call erase on all of them */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::erase_after(KeyframeContainer::elem_ptr last_valid) { // exclude the last_valid element from deletion @@ -514,7 +596,7 @@ KeyframeContainer::erase_after(KeyframeContainer::elem_ptr last_valid) { /* * Delete the element from the list and call delete on it. */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::erase(KeyframeContainer::elem_ptr e) { this->container.erase(this->begin() + e); @@ -522,29 +604,29 @@ KeyframeContainer::erase(KeyframeContainer::elem_ptr e) { } -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::sync(const KeyframeContainer &other, const time::time_t &start) { - // Delete elements after start time + // Delete elements from this container after start time elem_ptr at = this->last_before(start, this->container.size()); at = this->erase_after(at); - auto at_other = 1; // always skip the first element (because it's the default value) + // Find the last element before the start time in the other container + elem_ptr at_other = other.last_before(start, other.size()); + ++at_other; // move one element forward so that at_other.time() >= start // Copy all elements from other with time >= start - for (size_t i = at_other; i < other.size(); i++) { - if (other.get(i).time() >= start) { - at = this->insert_after(other.get(i), at); - } - } + this->container.insert(this->container.end(), + other.container.begin() + at_other, + other.container.end()); return this->container.size(); } -template -template +template +template typename KeyframeContainer::elem_ptr KeyframeContainer::sync(const KeyframeContainer &other, const std::function &converter, @@ -553,20 +635,21 @@ KeyframeContainer::sync(const KeyframeContainer &other, elem_ptr at = this->last_before(start, this->container.size()); at = this->erase_after(at); - auto at_other = 1; // always skip the first element (because it's the default value) + // Find the last element before the start time in the other container + elem_ptr at_other = other.last_before(start, other.size()); + ++at_other; // move one element forward so that at_other.time() >= start // Copy all elements from other with time >= start for (size_t i = at_other; i < other.size(); i++) { - if (other.get(i).time() >= start) { - at = this->insert_after(keyframe_t(other.get(i).time(), converter(other.get(i).val())), at); - } + auto &elem = other.get(i); + this->container.emplace_back(elem.time(), converter(elem.val())); } return this->container.size(); } -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::erase_group(const time::time_t &time, const KeyframeContainer::elem_ptr &last_elem) { diff --git a/libopenage/curve/map.h b/libopenage/curve/map.h index 9712c89470..49724cad69 100644 --- a/libopenage/curve/map.h +++ b/libopenage/curve/map.h @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -7,6 +7,7 @@ #include #include +#include "curve/concept.h" #include "curve/map_filter_iterator.h" #include "time/time.h" #include "util/fixed_point.h" @@ -18,7 +19,7 @@ namespace openage::curve { * Map that keeps track of the lifetime of the contained elements. * Make sure that no key is reused. */ -template +template class UnorderedMap { /** Internal container to access all data and metadata */ struct map_element { @@ -83,14 +84,14 @@ class UnorderedMap { } }; -template +template std::optional>> UnorderedMap::operator()(const time::time_t &time, const key_t &key) const { return this->at(time, key); } -template +template std::optional>> UnorderedMap::at(const time::time_t &time, const key_t &key) const { @@ -107,7 +108,7 @@ UnorderedMap::at(const time::time_t &time, } } -template +template MapFilterIterator> UnorderedMap::begin(const time::time_t &time) const { return MapFilterIterator>( @@ -117,7 +118,7 @@ UnorderedMap::begin(const time::time_t &time) const { time::TIME_MAX); } -template +template MapFilterIterator> UnorderedMap::end(const time::time_t &time) const { return MapFilterIterator>( @@ -127,7 +128,7 @@ UnorderedMap::end(const time::time_t &time) const { time); } -template +template MapFilterIterator> UnorderedMap::between(const time::time_t &from, const time::time_t &to) const { auto it = MapFilterIterator>( @@ -142,7 +143,7 @@ UnorderedMap::between(const time::time_t &from, const time::time_t return it; } -template +template MapFilterIterator> UnorderedMap::insert(const time::time_t &alive, const key_t &key, @@ -154,7 +155,7 @@ UnorderedMap::insert(const time::time_t &alive, value); } -template +template MapFilterIterator> UnorderedMap::insert(const time::time_t &alive, const time::time_t &dead, @@ -169,7 +170,7 @@ UnorderedMap::insert(const time::time_t &alive, dead); } -template +template void UnorderedMap::birth(const time::time_t &time, const key_t &key) { auto it = this->container.find(key); @@ -178,13 +179,13 @@ void UnorderedMap::birth(const time::time_t &time, } } -template +template void UnorderedMap::birth(const time::time_t &time, const MapFilterIterator &it) { it->second.alive = time; } -template +template void UnorderedMap::kill(const time::time_t &time, const key_t &key) { auto it = this->container.find(key); @@ -193,13 +194,13 @@ void UnorderedMap::kill(const time::time_t &time, } } -template +template void UnorderedMap::kill(const time::time_t &time, const MapFilterIterator &it) { it->second.dead = time; } -template +template void UnorderedMap::clean(const time::time_t &) { // TODO save everything to a file and be happy. } diff --git a/libopenage/curve/map_filter_iterator.h b/libopenage/curve/map_filter_iterator.h index 5e71c0789f..0e2e98f813 100644 --- a/libopenage/curve/map_filter_iterator.h +++ b/libopenage/curve/map_filter_iterator.h @@ -1,7 +1,8 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once +#include "curve/concept.h" #include "curve/iterator.h" #include "time/time.h" @@ -16,8 +17,8 @@ namespace openage::curve { * It depends on key_t and val_t as map-parameters, container_t is the container * to operate on and the function valid_f, that checks if an element is alive. */ -template class MapFilterIterator : public CurveIterator { public: diff --git a/libopenage/curve/queue.h b/libopenage/curve/queue.h index 9604a76308..9e652b92e1 100644 --- a/libopenage/curve/queue.h +++ b/libopenage/curve/queue.h @@ -11,6 +11,7 @@ #include "error/error.h" +#include "curve/concept.h" #include "curve/iterator.h" #include "curve/queue_filter_iterator.h" #include "event/evententity.h" @@ -30,7 +31,7 @@ namespace curve { * time it will happen. * This container can be used to store interactions */ -template +template class Queue : public event::EventEntity { struct queue_wrapper { // Insertion time of the element @@ -269,7 +270,7 @@ class Queue : public event::EventEntity { }; -template +template typename Queue::elem_ptr Queue::first_alive(const time::time_t &time) const { elem_ptr hint = 0; @@ -293,7 +294,7 @@ typename Queue::elem_ptr Queue::first_alive(const time::time_t &time) cons } -template +template const T &Queue::front(const time::time_t &time) const { elem_ptr at = this->first_alive(time); ENSURE(at < this->container.size(), @@ -308,7 +309,7 @@ const T &Queue::front(const time::time_t &time) const { } -template +template const T &Queue::pop_front(const time::time_t &time) { elem_ptr at = this->first_alive(time); ENSURE(at < this->container.size(), @@ -334,7 +335,7 @@ const T &Queue::pop_front(const time::time_t &time) { } -template +template bool Queue::empty(const time::time_t &time) const { if (this->container.empty()) { return true; @@ -344,7 +345,7 @@ bool Queue::empty(const time::time_t &time) const { } -template +template QueueFilterIterator> Queue::begin(const time::time_t &t) const { for (auto it = this->container.begin(); it != this->container.end(); ++it) { if (it->alive() >= t) { @@ -360,7 +361,7 @@ QueueFilterIterator> Queue::begin(const time::time_t &t) const { } -template +template QueueFilterIterator> Queue::end(const time::time_t &t) const { return QueueFilterIterator>( container.end(), @@ -370,7 +371,7 @@ QueueFilterIterator> Queue::end(const time::time_t &t) const { } -template +template QueueFilterIterator> Queue::between(const time::time_t &begin, const time::time_t &end) const { auto it = QueueFilterIterator>( @@ -385,20 +386,20 @@ QueueFilterIterator> Queue::between(const time::time_t &begin, } -template +template void Queue::erase(const CurveIterator> &it) { container.erase(it.get_base()); } -template +template void Queue::kill(const time::time_t &time, elem_ptr at) { this->container[at].set_dead(time); } -template +template QueueFilterIterator> Queue::insert(const time::time_t &time, const T &e) { elem_ptr at = this->container.size(); @@ -442,7 +443,7 @@ QueueFilterIterator> Queue::insert(const time::time_t &time, } -template +template void Queue::clear(const time::time_t &time) { elem_ptr at = this->first_alive(time); diff --git a/libopenage/curve/queue_filter_iterator.h b/libopenage/curve/queue_filter_iterator.h index 248abb44ad..36dff630a7 100644 --- a/libopenage/curve/queue_filter_iterator.h +++ b/libopenage/curve/queue_filter_iterator.h @@ -2,6 +2,7 @@ #pragma once +#include "curve/concept.h" #include "curve/iterator.h" #include "time/time.h" @@ -16,7 +17,7 @@ namespace openage::curve { * It depends on val_t as its value type, container_t is the container * to operate on and the function valid_f, that checks if an element is alive. */ -template class QueueFilterIterator : public CurveIterator { public: diff --git a/libopenage/curve/segmented.h b/libopenage/curve/segmented.h index 98add2995e..2d1ced622b 100644 --- a/libopenage/curve/segmented.h +++ b/libopenage/curve/segmented.h @@ -5,6 +5,7 @@ #include #include +#include "curve/concept.h" #include "curve/interpolated.h" #include "time/time.h" @@ -25,7 +26,7 @@ namespace openage::curve { * The bound template type T has to implement `operator +(T)` and * `operator *(time::time_t)`. */ -template +template class Segmented : public Interpolated { public: using Interpolated::Interpolated; @@ -49,7 +50,7 @@ class Segmented : public Interpolated { }; -template +template void Segmented::set_insert_jump(const time::time_t &at, const T &leftval, const T &rightval) { auto hint = this->container.insert_overwrite(at, leftval, this->last_element, true); this->container.insert_after(at, rightval, hint); @@ -57,7 +58,7 @@ void Segmented::set_insert_jump(const time::time_t &at, const T &leftval, con } -template +template void Segmented::set_last_jump(const time::time_t &at, const T &leftval, const T &rightval) { auto hint = this->container.last(at, this->last_element); @@ -76,7 +77,7 @@ void Segmented::set_last_jump(const time::time_t &at, const T &leftval, const } -template +template std::string Segmented::idstr() const { std::stringstream ss; ss << "SegmentedCurve["; diff --git a/libopenage/curve/tests/curve_types.cpp b/libopenage/curve/tests/curve_types.cpp index 421d3fb4d7..3a0ecb9b0e 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -232,7 +232,43 @@ void curve_types() { TESTEQUALS(c.get(8), 4); } - //Check the discrete type + { + // compression + auto f = std::make_shared(); + Continuous> c(f, 0); + c.set_insert(0, 0); + c.set_insert(1, 1); // redundant + c.set_insert(2, 2); // redundant + c.set_insert(3, 3); + c.set_insert(4, 3); // redundant + c.set_insert(5, 3); + c.set_insert(6, 4); + c.set_insert(7, 4); + + auto frame0 = c.frame(2); + TESTEQUALS(frame0.first, 2); + TESTEQUALS(frame0.second, 2); + TESTEQUALS(c.get(2), 2); + + auto frame1 = c.frame(4); + TESTEQUALS(frame1.first, 4); + TESTEQUALS(frame1.second, 3); + TESTEQUALS(c.get(4), 3); + + c.compress(0); + + auto frame2 = c.frame(2); + TESTEQUALS(frame2.first, 0); + TESTEQUALS(frame2.second, 0); + TESTEQUALS(c.get(2), 2); + + auto frame3 = c.frame(4); + TESTEQUALS(frame3.first, 3); + TESTEQUALS(frame3.second, 3); + TESTEQUALS(c.get(4), 3); + } + + // Check the discrete type { auto f = std::make_shared(); Discrete c(f, 0); @@ -257,7 +293,41 @@ void curve_types() { TESTEQUALS(complex.get(10), "Test 10"); } - //Check the discrete mod type + { + // compression + auto f = std::make_shared(); + Discrete c(f, 0); + c.set_insert(0, 1); + c.set_insert(1, 3); + c.set_insert(2, 3); // redundant + c.set_insert(3, 3); // redundant + c.set_insert(4, 4); + c.set_insert(5, 4); // redundant + + auto frame0 = c.frame(2); + TESTEQUALS(frame0.first, 2); + TESTEQUALS(frame0.second, 3); + TESTEQUALS(c.get(2), 3); + + auto frame1 = c.frame(5); + TESTEQUALS(frame1.first, 5); + TESTEQUALS(frame1.second, 4); + TESTEQUALS(c.get(5), 4); + + c.compress(0); + + auto frame2 = c.frame(2); + TESTEQUALS(frame2.first, 1); + TESTEQUALS(frame2.second, 3); + TESTEQUALS(c.get(2), 3); + + auto frame3 = c.frame(5); + TESTEQUALS(frame3.first, 4); + TESTEQUALS(frame3.second, 4); + TESTEQUALS(c.get(5), 4); + } + + // Check the discrete mod type { auto f = std::make_shared(); DiscreteMod c(f, 0); @@ -290,7 +360,7 @@ void curve_types() { TESTEQUALS(c.get_mod(15, 0), 0); } - //check set_last + // check set_last { auto f = std::make_shared(); Discrete c(f, 0); diff --git a/libopenage/event/demo/gamestate.h b/libopenage/event/demo/gamestate.h index b92b22ee93..c4ba52c6e2 100644 --- a/libopenage/event/demo/gamestate.h +++ b/libopenage/event/demo/gamestate.h @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2024 the openage authors. See copying.md for legal info. #pragma once @@ -45,6 +45,10 @@ class PongEvent { PongEvent() : player(0), state(IDLE) {} + bool operator==(const PongEvent &other) const { + return this->player == other.player and this->state == other.state; + } + size_t player; state_e state; }; diff --git a/libopenage/gamestate/activity/CMakeLists.txt b/libopenage/gamestate/activity/CMakeLists.txt index 78a78e7ab0..408c88681e 100644 --- a/libopenage/gamestate/activity/CMakeLists.txt +++ b/libopenage/gamestate/activity/CMakeLists.txt @@ -9,7 +9,9 @@ add_sources(libopenage types.cpp xor_event_gate.cpp xor_gate.cpp + xor_switch_gate.cpp ) add_subdirectory("event") add_subdirectory("condition") +add_subdirectory("tests") diff --git a/libopenage/gamestate/activity/condition/CMakeLists.txt b/libopenage/gamestate/activity/condition/CMakeLists.txt index aabd159b0c..c4ba029b27 100644 --- a/libopenage/gamestate/activity/condition/CMakeLists.txt +++ b/libopenage/gamestate/activity/condition/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage command_in_queue.cpp + next_command_switch.cpp next_command.cpp ) diff --git a/libopenage/gamestate/activity/condition/next_command.cpp b/libopenage/gamestate/activity/condition/next_command.cpp index c0b619a783..f7fb4b9e24 100644 --- a/libopenage/gamestate/activity/condition/next_command.cpp +++ b/libopenage/gamestate/activity/condition/next_command.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "next_command.h" @@ -34,4 +34,17 @@ bool next_command_move(const time::time_t &time, return command->get_type() == component::command::command_t::MOVE; } +bool next_command_apply_effect(const time::time_t &time, + const std::shared_ptr &entity) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + if (command_queue->get_queue().empty(time)) { + return false; + } + + auto command = command_queue->get_queue().front(time); + return command->get_type() == component::command::command_t::APPLY_EFFECT; +} + } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/next_command.h b/libopenage/gamestate/activity/condition/next_command.h index 046a18cec6..251cf9e243 100644 --- a/libopenage/gamestate/activity/condition/next_command.h +++ b/libopenage/gamestate/activity/condition/next_command.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -35,5 +35,16 @@ bool next_command_idle(const time::time_t &time, bool next_command_move(const time::time_t &time, const std::shared_ptr &entity); +/** + * Condition for next command check in the activity system. + * + * @param time Time when the condition is checked. + * @param entity Game entity. + * + * @return true if the entity has an apply effect command next in the queue, false otherwise. + */ +bool next_command_apply_effect(const time::time_t &time, + const std::shared_ptr &entity); + } // namespace activity } // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/condition/next_command_switch.cpp b/libopenage/gamestate/activity/condition/next_command_switch.cpp new file mode 100644 index 0000000000..be0afd1543 --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command_switch.cpp @@ -0,0 +1,25 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "next_command_switch.h" + +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/game_entity.h" + + +namespace openage::gamestate::activity { + +int next_command_switch(const time::time_t &time, + const std::shared_ptr &entity) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + if (command_queue->get_queue().empty(time)) { + return -1; + } + + auto command = command_queue->get_queue().front(time); + + return static_cast(command->get_type()); +} + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/next_command_switch.h b/libopenage/gamestate/activity/condition/next_command_switch.h new file mode 100644 index 0000000000..ba97d5b322 --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command_switch.h @@ -0,0 +1,23 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace activity { + +/** + * Returns true if the next command in the queue is an idle command. + */ +int next_command_switch(const time::time_t &time, + const std::shared_ptr &entity); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/tests/CMakeLists.txt b/libopenage/gamestate/activity/tests/CMakeLists.txt new file mode 100644 index 0000000000..a4d787ae45 --- /dev/null +++ b/libopenage/gamestate/activity/tests/CMakeLists.txt @@ -0,0 +1,3 @@ +add_sources(libopenage + node_types.cpp +) diff --git a/libopenage/gamestate/activity/tests/node_types.cpp b/libopenage/gamestate/activity/tests/node_types.cpp new file mode 100644 index 0000000000..702143b251 --- /dev/null +++ b/libopenage/gamestate/activity/tests/node_types.cpp @@ -0,0 +1,251 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include + +#include "gamestate/activity/end_node.h" +#include "gamestate/activity/start_node.h" +#include "gamestate/activity/task_node.h" +#include "gamestate/activity/task_system_node.h" +#include "gamestate/activity/types.h" +#include "gamestate/activity/xor_event_gate.h" +#include "gamestate/activity/xor_gate.h" +#include "gamestate/activity/xor_switch_gate.h" +#include "gamestate/system/types.h" +#include "testing/testing.h" + + +namespace openage::gamestate::activity::tests { + +/** + * Unit tests for the activity node types. + */ +void node_types() { + // Start node type + { + auto start_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(start_node->get_id(), 0); + TESTEQUALS(static_cast(start_node->get_type()), static_cast(node_t::START)); + TESTEQUALS(start_node->get_label(), "Start"); + TESTEQUALS(start_node->str(), "Start (id=0)"); + + auto next_node = std::make_shared(1); + start_node->add_output(next_node); + + // Check the next node + TESTEQUALS(start_node->get_next(), 1); + TESTEQUALS(start_node->next(1), next_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(start_node->next(999)); + } + + // End node type + { + auto end_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(end_node->get_id(), 0); + // TESTEQUALS(end_node->get_type(), node_t::END); + TESTEQUALS(end_node->get_label(), "End"); + TESTEQUALS(end_node->str(), "End (id=0)"); + + // End nodes have no outputs + TESTTHROWS(end_node->next(999)); + } + + // Task system node type + { + auto task_system_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(task_system_node->get_id(), 0); + // TESTEQUALS(task_system_node->get_type(), node_t::TASK_SYSTEM); + TESTEQUALS(task_system_node->get_label(), "TaskSystem"); + TESTEQUALS(task_system_node->str(), "TaskSystem (id=0)"); + + auto next_node = std::make_shared(1); + task_system_node->add_output(next_node); + + // Check the next node + TESTEQUALS(task_system_node->get_next(), 1); + TESTEQUALS(task_system_node->next(1), next_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(task_system_node->next(999)); + + auto sytem_id = system::system_id_t::MOVE_DEFAULT; + task_system_node->set_system_id(sytem_id); + + // Check the system ID + TESTEQUALS(static_cast(task_system_node->get_system_id()), static_cast(sytem_id)); + } + + // Task custom node type + { + auto task_custom_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(task_custom_node->get_id(), 0); + // TESTEQUALS(task_custom_node->get_type(), node_t::TASK_CUSTOM); + TESTEQUALS(task_custom_node->get_label(), "TaskCustom"); + TESTEQUALS(task_custom_node->str(), "TaskCustom (id=0)"); + + auto next_node = std::make_shared(1); + task_custom_node->add_output(next_node); + + // Check the next node + TESTEQUALS(task_custom_node->get_next(), 1); + TESTEQUALS(task_custom_node->next(1), next_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(task_custom_node->next(999)); + + auto task_func = [](const time::time_t & /* time */, const std::shared_ptr & /* entity */) { + // Do nothing + }; + task_custom_node->set_task_func(task_func); + + // Check the task function + auto get_func = task_custom_node->get_task_func(); + get_func(time::time_t{0}, nullptr); + } + + // Xor gate node type + { + auto xor_gate_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(xor_gate_node->get_id(), 0); + // TESTEQUALS(xor_gate_node->get_type(), node_t::XOR_GATE); + TESTEQUALS(xor_gate_node->get_label(), "ExclusiveGateway"); + TESTEQUALS(xor_gate_node->str(), "ExclusiveGateway (id=0)"); + + auto default_node = std::make_shared(1); + xor_gate_node->set_default(default_node); + + // Check the default node + TESTEQUALS(xor_gate_node->get_default(), default_node); + + auto option1_node = std::make_shared(2); + xor_gate_node->add_output(option1_node, [](const time::time_t &time, const std::shared_ptr & /* entity */) { + return time == time::TIME_ZERO; + }); + + auto option2_node = std::make_shared(3); + xor_gate_node->add_output(option2_node, [](const time::time_t &time, const std::shared_ptr & /* entity */) { + return time == time::TIME_MAX; + }); + + auto conditions = xor_gate_node->get_conditions(); + + // Check the conditions + TESTEQUALS(conditions.size(), 2); + + TESTEQUALS(conditions.at(2)(time::TIME_ZERO, nullptr), true); + TESTEQUALS(conditions.at(3)(time::TIME_ZERO, nullptr), false); + + TESTEQUALS(conditions.at(2)(time::TIME_MAX, nullptr), false); + TESTEQUALS(conditions.at(3)(time::TIME_MAX, nullptr), true); + + // Check if next nodes return correctly + TESTEQUALS(xor_gate_node->next(1), default_node); + TESTEQUALS(xor_gate_node->next(2), option1_node); + TESTEQUALS(xor_gate_node->next(3), option2_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(xor_gate_node->next(999)); + } + + // Xor switch gate node type + { + auto xor_switch_gate_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(xor_switch_gate_node->get_id(), 0); + // TESTEQUALS(xor_switch_gate_node->get_type(), node_t::XOR_SWITCH_GATE); + TESTEQUALS(xor_switch_gate_node->get_label(), "ExclusiveSwitchGateway"); + TESTEQUALS(xor_switch_gate_node->str(), "ExclusiveSwitchGateway (id=0)"); + + auto default_node = std::make_shared(1); + xor_switch_gate_node->set_default(default_node); + + // Check the default node + TESTEQUALS(xor_switch_gate_node->get_default(), default_node); + + auto option1_node = std::make_shared(2); + xor_switch_gate_node->set_output(option1_node, 1); + + auto option2_node = std::make_shared(3); + xor_switch_gate_node->set_output(option2_node, 2); + + auto lookup_func = [](const time::time_t &time, const std::shared_ptr & /* entity */) { + if (time == time::TIME_ZERO) { + return 1; + } + if (time == time::TIME_MAX) { + return 2; + } + + return 0; + }; + xor_switch_gate_node->set_lookup_func(lookup_func); + + // Check the lookup function + TESTEQUALS(xor_switch_gate_node->get_lookup_func()(time::TIME_ZERO, nullptr), 1); + TESTEQUALS(xor_switch_gate_node->get_lookup_func()(time::TIME_MAX, nullptr), 2); + TESTEQUALS(xor_switch_gate_node->get_lookup_func()(time::TIME_MIN, nullptr), 0); + + auto lookup_dict = xor_switch_gate_node->get_lookup_dict(); + + // Check the lookup dict + TESTEQUALS(lookup_dict.size(), 2); + + TESTEQUALS(lookup_dict.at(1), option1_node); + TESTEQUALS(lookup_dict.at(2), option2_node); + + // Check if next nodes return correctly + TESTEQUALS(xor_switch_gate_node->next(1), default_node); + TESTEQUALS(xor_switch_gate_node->next(2), option1_node); + TESTEQUALS(xor_switch_gate_node->next(3), option2_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(xor_switch_gate_node->next(999)); + } + + // Xor event gate node type + { + auto xor_event_gate_node = std::make_shared(0); + + // Check basic properties inherited from Node interface + TESTEQUALS(xor_event_gate_node->get_id(), 0); + // TESTEQUALS(xor_event_gate_node->get_type(), node_t::XOR_EVENT_GATE); + TESTEQUALS(xor_event_gate_node->get_label(), "ExclusiveEventGateway"); + TESTEQUALS(xor_event_gate_node->str(), "ExclusiveEventGateway (id=0)"); + + auto option1_node = std::make_shared(1); + xor_event_gate_node->add_output(option1_node, [](const time::time_t & /* time */, const std::shared_ptr & /* entity */, const std::shared_ptr & /* loop */, const std::shared_ptr & /* state */, size_t /* next_id */) { + return nullptr; + }); + + auto option2_node = std::make_shared(2); + xor_event_gate_node->add_output(option2_node, [](const time::time_t & /* time */, const std::shared_ptr & /* entity */, const std::shared_ptr & /* loop */, const std::shared_ptr & /* state */, size_t /* next_id */) { + return nullptr; + }); + + auto primers = xor_event_gate_node->get_primers(); + + // Check the primers + TESTEQUALS(primers.size(), 2); + + // Check if next nodes return correctly + TESTEQUALS(xor_event_gate_node->next(1), option1_node); + TESTEQUALS(xor_event_gate_node->next(2), option2_node); + + // Check that the node throws errors for invalid output IDs + TESTTHROWS(xor_event_gate_node->next(999)); + } +} + +} // namespace openage::gamestate::activity::tests diff --git a/libopenage/gamestate/activity/types.h b/libopenage/gamestate/activity/types.h index ac2189e5ab..f9e630171b 100644 --- a/libopenage/gamestate/activity/types.h +++ b/libopenage/gamestate/activity/types.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -13,6 +13,7 @@ enum class node_t { END, XOR_EVENT_GATE, XOR_GATE, + XOR_SWITCH_GATE, TASK_CUSTOM, TASK_SYSTEM, }; diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index 38c8ccacb1..64e195faf3 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -61,7 +61,7 @@ class XorEventGate : public Node { * @param label Human-readable label (optional). */ XorEventGate(node_id_t id, - node_label_t label = "EventGateWay"); + node_label_t label = "ExclusiveEventGateway"); /** * Create a new exclusive event gateway. diff --git a/libopenage/gamestate/activity/xor_gate.h b/libopenage/gamestate/activity/xor_gate.h index f73287ed86..0ced1d6d27 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.h @@ -24,7 +24,7 @@ namespace activity { /** * Function that determines if an output node is chosen. * - * @param time Current game time. + * @param time Current simulation time. * @param entity Entity that is executing the activity. * * @return true if the output node is chosen, false otherwise. diff --git a/libopenage/gamestate/activity/xor_switch_gate.cpp b/libopenage/gamestate/activity/xor_switch_gate.cpp new file mode 100644 index 0000000000..1ae1445e09 --- /dev/null +++ b/libopenage/gamestate/activity/xor_switch_gate.cpp @@ -0,0 +1,53 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "xor_switch_gate.h" + + +namespace openage::gamestate::activity { + +XorSwitchGate::XorSwitchGate(node_id_t id, + node_label_t label) : + Node{id, label} {} + +XorSwitchGate::XorSwitchGate(node_id_t id, + node_label_t label, + const lookup_function_t &lookup_func, + const lookup_dict_t &lookup_dict, + const std::shared_ptr &default_node) : + Node{id, label}, + lookup_func{lookup_func}, + lookup_dict{lookup_dict}, + default_node{default_node} {} + +void XorSwitchGate::set_output(const std::shared_ptr &output, + const lookup_key_t &key) { + this->outputs.emplace(output->get_id(), output); + this->lookup_dict.emplace(key, output); +} + +const XorSwitchGate::lookup_function_t &XorSwitchGate::get_lookup_func() const { + return this->lookup_func; +} + +void XorSwitchGate::set_lookup_func(const lookup_function_t &lookup_func) { + this->lookup_func = lookup_func; +} + +const XorSwitchGate::lookup_dict_t &XorSwitchGate::get_lookup_dict() const { + return this->lookup_dict; +} + +const std::shared_ptr &XorSwitchGate::get_default() const { + return this->default_node; +} + +void XorSwitchGate::set_default(const std::shared_ptr &node) { + if (this->default_node != nullptr) { + throw Error{MSG(err) << this->str() << " already has a default node"}; + } + + this->outputs.emplace(node->get_id(), node); + this->default_node = node; +} + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_switch_gate.h b/libopenage/gamestate/activity/xor_switch_gate.h new file mode 100644 index 0000000000..371c11651e --- /dev/null +++ b/libopenage/gamestate/activity/xor_switch_gate.h @@ -0,0 +1,152 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "gamestate/activity/node.h" +#include "gamestate/activity/types.h" +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace activity { + +/** + * Chooses one of its output nodes based on enum values. + * + * In comparison to the XOR gate, this node type does not tie individual + * conditions to each node. Instead, it operates on a single function that + * returns a key for a lookup dict. The lookup dict maps these keys to output + * node ID. + * + * This type of gate is easier to use for simpler branch switches based on + * similar conditions, e.g. a branching based on the value of an enum. + */ +class XorSwitchGate : public Node { +public: + /** + * Type used as lookup key for the lookup dict. + */ + using lookup_key_t = int; + + /** + * Function that retrieves a lookup key for the lookup dict from + * the current state of an entity. + * + * @param time Current simulation time. + * @param entity Entity that is executing the activity. + * + * @return Lookup key. + */ + using lookup_function_t = std::function &)>; + + /** + * Lookup dict that maps lookup keys to output node IDs. + */ + using lookup_dict_t = std::unordered_map>; + + /** + * Creates a new XOR switch gate node. + * + * @param id Unique identifier of the node. + * @param label Human-readable label of the node (optional). + */ + XorSwitchGate(node_id_t id, + node_label_t label = "ExclusiveSwitchGateway"); + + /** + * Creates a new XOR switch gate node. + * + * @param id Unique identifier of the node. + * @param label Human-readable label of the node. + * @param lookup_func Function that looks up the key to the lookup dict. + * @param lookup_dict Initial lookup dict that maps lookup keys to output node IDs. + * @param default_node Default output node. Chosen if no lookup entry is defined. + */ + XorSwitchGate(node_id_t id, + node_label_t label, + const lookup_function_t &lookup_func, + const lookup_dict_t &lookup_dict, + const std::shared_ptr &default_node); + + virtual ~XorSwitchGate() = default; + + inline node_t get_type() const override { + return node_t::XOR_SWITCH_GATE; + } + + /** + * Set the output node for a given lookup key. + * + * @param output Output node. + * @param key Enumeration value. + */ + void set_output(const std::shared_ptr &output, + const lookup_key_t &key); + + /** + * Get the lookup function for determining the output nodes. + * + * @return Lookup function. + */ + const lookup_function_t &get_lookup_func() const; + + /** + * Set the lookup function for determining the output nodes. + * + * @param lookup_func Lookup function. + */ + void set_lookup_func(const lookup_function_t &lookup_func); + + /** + * Get the lookup dict for the output nodes. + * + * @return Lookup dict. + */ + const lookup_dict_t &get_lookup_dict() const; + + /** + * Get the default output node. + * + * @return Default output node. + */ + const std::shared_ptr &get_default() const; + + /** + * Set the the default output node. + * + * This node is chosen if the lookup dict does not contain an entry for the + * lookup key returned by the lookup function. + * + * @param node Default output node. + */ + void set_default(const std::shared_ptr &node); + +private: + /** + * Determines the lookup key for the lookup dict from the current state. + */ + lookup_function_t lookup_func; + + /** + * Maps lookup keys to output node IDs. + */ + lookup_dict_t lookup_dict; + + /** + * Default output node. Chosen if no lookup entry is defined. + */ + std::shared_ptr default_node; +}; + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/api/CMakeLists.txt b/libopenage/gamestate/api/CMakeLists.txt index f3ad8d7b40..c6735e759a 100644 --- a/libopenage/gamestate/api/CMakeLists.txt +++ b/libopenage/gamestate/api/CMakeLists.txt @@ -3,9 +3,11 @@ add_sources(libopenage activity.cpp animation.cpp definitions.cpp + effect.cpp patch.cpp player_setup.cpp property.cpp + resistance.cpp sound.cpp terrain.cpp types.cpp diff --git a/libopenage/gamestate/api/ability.cpp b/libopenage/gamestate/api/ability.cpp index 49ecfad48f..1fc24c064b 100644 --- a/libopenage/gamestate/api/ability.cpp +++ b/libopenage/gamestate/api/ability.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "ability.h" @@ -16,19 +16,31 @@ namespace openage::gamestate::api { bool APIAbility::is_ability(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.ability.Ability"; + for (auto &parent : obj.get_parents()) { + if (parent == "engine.ability.Ability") { + return true; + } + } + + return false; +} + +bool APIAbility::check_type(const nyan::Object &ability, + const ability_t &type) { + nyan::fqon_t immediate_parent = ability.get_parents()[0]; + nyan::ValueHolder ability_type = ABILITY_DEFS.get(type); + + std::shared_ptr ability_val = std::dynamic_pointer_cast( + ability_type.get_ptr()); + + return ability_val->get_name() == immediate_parent; } bool APIAbility::check_property(const nyan::Object &ability, const ability_property_t &property) { std::shared_ptr properties = ability.get("Ability.properties"); nyan::ValueHolder property_type = ABILITY_PROPERTY_DEFS.get(property); - if (properties->contains(property_type)) { - return true; - } - - return false; + return properties->contains(property_type); } diff --git a/libopenage/gamestate/api/ability.h b/libopenage/gamestate/api/ability.h index d0ba6ea54e..1df65a95fd 100644 --- a/libopenage/gamestate/api/ability.h +++ b/libopenage/gamestate/api/ability.h @@ -22,6 +22,17 @@ class APIAbility { */ static bool is_ability(const nyan::Object &obj); + /** + * Check if an ability is of a given type. + * + * @param ability \p Ability nyan object (type == \p engine.ability.Ability). + * @param type Ability type. + * + * @return true if the ability is of the given type, else false. + */ + static bool check_type(const nyan::Object &ability, + const ability_t &type); + /** * Check if an ability has a given property. * diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index edd596abe8..0b1b1c1fd9 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -78,6 +78,29 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { return next_nodes; } + case activity::node_t::XOR_SWITCH_GATE: { + auto switch_condition = node.get("XORSwitchGate.switch"); + std::shared_ptr db_view = node.get_view(); + + auto switch_condition_obj = db_view->get_object(switch_condition->get_name()); + auto switch_condition_parent = switch_condition_obj.get_parents()[0]; + auto switch_condition_type = ACTIVITY_SWITCH_CONDITION_TYPES.get(switch_condition_parent); + + switch (switch_condition_type) { + case switch_condition_t::NEXT_COMMAND: { + auto next = switch_condition_obj.get_dict("NextCommand.next"); + std::vector next_nodes; + for (auto next_node : next) { + auto next_node_value = std::dynamic_pointer_cast(next_node.second.get_ptr()); + next_nodes.push_back(db_view->get_object(next_node_value->get_name())); + } + + return next_nodes; + } + default: + throw Error(MSG(err) << "Unknown switch condition type."); + } + } default: throw Error(MSG(err) << "Unknown activity node type."); } diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index f982571f94..fa831b0e4a 100644 --- a/libopenage/gamestate/api/definitions.h +++ b/libopenage/gamestate/api/definitions.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -10,11 +10,13 @@ #include "datastructure/constexpr_map.h" #include "gamestate/activity/condition/command_in_queue.h" #include "gamestate/activity/condition/next_command.h" +#include "gamestate/activity/condition/next_command_switch.h" #include "gamestate/activity/event/command_in_queue.h" #include "gamestate/activity/event/wait.h" #include "gamestate/activity/types.h" #include "gamestate/activity/xor_event_gate.h" #include "gamestate/activity/xor_gate.h" +#include "gamestate/activity/xor_switch_gate.h" #include "gamestate/api/types.h" #include "gamestate/system/types.h" @@ -25,17 +27,168 @@ namespace openage::gamestate::api { * Maps internal ability types to nyan API values. */ static const auto ABILITY_DEFS = datastructure::create_const_map( + std::pair(ability_t::ACTIVITY, + nyan::ValueHolder(std::make_shared("engine.ability.type.Activity"))), + std::pair(ability_t::APPLY_CONTINUOUS_EFFECT, + nyan::ValueHolder(std::make_shared("engine.ability.type.ApplyContinuousEffect"))), + std::pair(ability_t::APPLY_DISCRETE_EFFECT, + nyan::ValueHolder(std::make_shared("engine.ability.type.ApplyDiscreteEffect"))), std::pair(ability_t::IDLE, nyan::ValueHolder(std::make_shared("engine.ability.type.Idle"))), std::pair(ability_t::MOVE, nyan::ValueHolder(std::make_shared("engine.ability.type.Move"))), + std::pair(ability_t::LINE_OF_SIGHT, + nyan::ValueHolder(std::make_shared("engine.ability.type.LineOfSight"))), std::pair(ability_t::LIVE, nyan::ValueHolder(std::make_shared("engine.ability.type.Live"))), + std::pair(ability_t::RANGED_CONTINUOUS_EFFECT, + nyan::ValueHolder(std::make_shared("engine.ability.type.RangedContinuousEffect"))), + std::pair(ability_t::RANGED_DISCRETE_EFFECT, + nyan::ValueHolder(std::make_shared("engine.ability.type.RangedDiscreteEffect"))), + std::pair(ability_t::RESISTANCE, + nyan::ValueHolder(std::make_shared("engine.ability.type.Resistance"))), + std::pair(ability_t::SELECTABLE, + nyan::ValueHolder(std::make_shared("engine.ability.type.Selectable"))), std::pair(ability_t::TURN, nyan::ValueHolder(std::make_shared("engine.ability.type.Turn")))); /** - * Maps internal property types to nyan API values. + * Maps internal effect types to nyan API values. + */ +static const auto EFFECT_DEFS = datastructure::create_const_map( + std::pair(effect_t::CONTINUOUS_FLAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_FLAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease"))), + std::pair(effect_t::CONTINUOUS_LURE, + nyan::ValueHolder(std::make_shared("engine.effect.continuous.type.Lure"))), + std::pair(effect_t::CONTINUOUS_TRAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_TRAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeChangeIncrease"))), + std::pair(effect_t::CONTINUOUS_TRPC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.time_relative_progress_change.type.TimeRelativeProgressChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_TRPC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.continuous.time_relative_progress_change.type.TimeRelativeProgressChangeIncrease"))), + std::pair(effect_t::DISCRETE_CONVERT, + nyan::ValueHolder(std::make_shared("engine.effect.discrete.Convert"))), + std::pair(effect_t::DISCRETE_FLAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease"))), + std::pair(effect_t::DISCRETE_FLAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease"))), + std::pair(effect_t::DISCRETE_MAKE_HARVESTABLE, + nyan::ValueHolder(std::make_shared("engine.effect.discrete.type.MakeHarvestable"))), + std::pair(effect_t::DISCRETE_SEND_TO_CONTAINER, + nyan::ValueHolder(std::make_shared("engine.effect.discrete.type.SendToContainer")))); + +/** + * Maps internal effect types to nyan API values. + */ +static const auto RESISTANCE_DEFS = datastructure::create_const_map( + std::pair(effect_t::CONTINUOUS_FLAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousFlatAttributeChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_FLAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousFlatAttributeChangeIncrease"))), + std::pair(effect_t::CONTINUOUS_LURE, + nyan::ValueHolder(std::make_shared("engine.resistance.type.Lure"))), + std::pair(effect_t::CONTINUOUS_TRAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousTimeRelativeAttributeChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_TRAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousTimeRelativeAttributeChangeIncrease"))), + std::pair(effect_t::CONTINUOUS_TRPC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousTimeRelativeProgressChangeDecrease"))), + std::pair(effect_t::CONTINUOUS_TRPC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.ContinuousTimeRelativeProgressChangeIncrease"))), + std::pair(effect_t::DISCRETE_CONVERT, + nyan::ValueHolder(std::make_shared("engine.resistance.type.Convert"))), + std::pair(effect_t::DISCRETE_FLAC_DECREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.DiscreteFlatAttributeChangeDecrease"))), + std::pair(effect_t::DISCRETE_FLAC_INCREASE, + nyan::ValueHolder(std::make_shared( + "engine.resistance.type.DiscreteFlatAttributeChangeIncrease"))), + std::pair(effect_t::DISCRETE_MAKE_HARVESTABLE, + nyan::ValueHolder(std::make_shared("engine.resistance.type.MakeHarvestable"))), + std::pair(effect_t::DISCRETE_SEND_TO_CONTAINER, + nyan::ValueHolder(std::make_shared("engine.resistance.type.SendToContainer")))); + +/** + * Maps API effect types to internal effect types. + */ +static const auto EFFECT_TYPE_LOOKUP = datastructure::create_const_map( + std::pair("engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease", + effect_t::CONTINUOUS_FLAC_DECREASE), + std::pair("engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease", + effect_t::CONTINUOUS_FLAC_INCREASE), + std::pair("engine.effect.continuous.type.Lure", + effect_t::CONTINUOUS_LURE), + std::pair("engine.effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeChangeDecrease", + effect_t::CONTINUOUS_TRAC_DECREASE), + std::pair("engine.effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeChangeIncrease", + effect_t::CONTINUOUS_TRAC_INCREASE), + std::pair("engine.effect.continuous.time_relative_progress_change.type.TimeRelativeProgressChangeDecrease", + effect_t::CONTINUOUS_TRPC_DECREASE), + std::pair("engine.effect.continuous.time_relative_progress_change.type.TimeRelativeProgressChangeIncrease", + effect_t::CONTINUOUS_TRPC_INCREASE), + std::pair("engine.effect.discrete.Convert", + effect_t::DISCRETE_CONVERT), + std::pair("engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease", + effect_t::DISCRETE_FLAC_DECREASE), + std::pair("engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease", + effect_t::DISCRETE_FLAC_INCREASE), + std::pair("engine.effect.discrete.type.MakeHarvestable", + effect_t::DISCRETE_MAKE_HARVESTABLE), + std::pair("engine.effect.discrete.type.SendToContainer", + effect_t::DISCRETE_SEND_TO_CONTAINER)); + +/** + * Maps API resistance types to internal effect types. + */ +static const auto RESISTANCE_TYPE_LOOKUP = datastructure::create_const_map( + std::pair("engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease", + effect_t::CONTINUOUS_FLAC_DECREASE), + std::pair("engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease", + effect_t::CONTINUOUS_FLAC_INCREASE), + std::pair("engine.resistance.continuous.type.Lure", + effect_t::CONTINUOUS_LURE), + std::pair("engine.resistance.continuous.type.TimeRelativeAttributeChangeDecrease", + effect_t::CONTINUOUS_TRAC_DECREASE), + std::pair("engine.resistance.continuous.type.TimeRelativeAttributeChangeIncrease", + effect_t::CONTINUOUS_TRAC_INCREASE), + std::pair("engine.resistance.continuous.type.TimeRelativeProgressChangeDecrease", + effect_t::CONTINUOUS_TRPC_DECREASE), + std::pair("engine.resistance.continuous.type.TimeRelativeProgressChangeIncrease", + effect_t::CONTINUOUS_TRPC_INCREASE), + std::pair("engine.resistance.discrete.type.Convert", + effect_t::DISCRETE_CONVERT), + std::pair("engine.resistance.discrete.convert.type.AoE2Convert", // TODO: Remove from API + effect_t::DISCRETE_CONVERT), + std::pair("engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease", + effect_t::DISCRETE_FLAC_DECREASE), + std::pair("engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease", + effect_t::DISCRETE_FLAC_INCREASE), + std::pair("engine.resistance.discrete.type.MakeHarvestable", + effect_t::DISCRETE_MAKE_HARVESTABLE), + std::pair("engine.resistance.discrete.type.SendToContainer", + effect_t::DISCRETE_SEND_TO_CONTAINER)); + + +/** + * Maps internal ability property types to nyan API values. */ static const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map( std::pair(ability_property_t::ANIMATED, @@ -51,6 +204,28 @@ static const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map("engine.ability.property.type.Lock")))); +/** + * Maps internal effect property types to nyan API values. + */ +static const auto EFFECT_PROPERTY_DEFS = datastructure::create_const_map( + std::pair(effect_property_t::AREA, + nyan::ValueHolder(std::make_shared("engine.effect.property.type.Area"))), + std::pair(effect_property_t::COST, + nyan::ValueHolder(std::make_shared("engine.effect.property.type.Cost"))), + std::pair(effect_property_t::DIPLOMATIC, + nyan::ValueHolder(std::make_shared("engine.effect.property.type.Diplomatic"))), + std::pair(effect_property_t::PRIORITY, + nyan::ValueHolder(std::make_shared("engine.effect.property.type.Priority")))); + +/** + * Maps internal resistance property types to nyan API values. + */ +static const auto RESISTANCE_PROPERTY_DEFS = datastructure::create_const_map( + std::pair(resistance_property_t::COST, + nyan::ValueHolder(std::make_shared("engine.resistance.property.type.Cost"))), + std::pair(resistance_property_t::STACKED, + nyan::ValueHolder(std::make_shared("engine.resistance.property.type.Stacked")))); + /** * Maps API activity node types to engine node types. */ @@ -64,7 +239,9 @@ static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( + std::pair("engine.ability.type.ApplyDiscreteEffect", + system::system_id_t::APPLY_EFFECT), std::pair("engine.ability.type.Idle", system::system_id_t::IDLE), std::pair("engine.ability.type.Move", @@ -83,11 +262,16 @@ static const auto ACTIVITY_TASK_SYSTEM_DEFS = datastructure::create_const_map( std::pair("engine.util.activity.condition.type.CommandInQueue", std::function(gamestate::activity::command_in_queue)), + std::pair("engine.util.activity.condition.type.NextCommandApplyEffect", + std::function(gamestate::activity::next_command_apply_effect)), std::pair("engine.util.activity.condition.type.NextCommandIdle", std::function(gamestate::activity::next_command_idle)), std::pair("engine.util.activity.condition.type.NextCommandMove", std::function(gamestate::activity::next_command_move))); +/** + * Maps API activity event types to event primer functions. + */ static const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map( std::pair("engine.util.activity.event.type.CommandInQueue", std::function(gamestate::activity::primer_command_in_queue)), @@ -96,6 +280,19 @@ static const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map( + std::pair("engine.util.activity.switch_condition.type.NextCommand", + std::function(gamestate::activity::next_command_switch))); + +static const auto ACTIVITY_SWITCH_CONDITION_TYPES = datastructure::create_const_map( + std::pair("engine.util.activity.switch_condition.type.NextCommand", + switch_condition_t::NEXT_COMMAND)); + /** * Maps internal patch property types to nyan API values. */ diff --git a/libopenage/gamestate/api/effect.cpp b/libopenage/gamestate/api/effect.cpp new file mode 100644 index 0000000000..691f97b58d --- /dev/null +++ b/libopenage/gamestate/api/effect.cpp @@ -0,0 +1,57 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "effect.h" + +#include "gamestate/api/definitions.h" + + +namespace openage::gamestate::api { + +bool APIEffect::is_effect(const nyan::Object &obj) { + for (auto &parent : obj.get_parents()) { + if (parent == "engine.effect.Effect") { + return true; + } + } + + return false; +} + +bool APIEffect::check_type(const nyan::Object &effect, + const effect_t &type) { + nyan::fqon_t immediate_parent = effect.get_parents()[0]; + nyan::ValueHolder effect_type = EFFECT_DEFS.get(type); + + std::shared_ptr effect_val = std::dynamic_pointer_cast( + effect_type.get_ptr()); + + return effect_val->get_name() == immediate_parent; +} + +bool APIEffect::check_property(const nyan::Object &effect, + const effect_property_t &property) { + std::shared_ptr properties = effect.get("Effect.properties"); + nyan::ValueHolder property_type = EFFECT_PROPERTY_DEFS.get(property); + + return properties->contains(property_type); +} + +effect_t APIEffect::get_type(const nyan::Object &effect) { + nyan::fqon_t immediate_parent = effect.get_parents()[0]; + + return EFFECT_TYPE_LOOKUP.get(immediate_parent); +} + +const nyan::Object APIEffect::get_property(const nyan::Object &effect, + const effect_property_t &property) { + std::shared_ptr properties = effect.get("Effect.properties"); + nyan::ValueHolder property_type = EFFECT_PROPERTY_DEFS.get(property); + + std::shared_ptr db_view = effect.get_view(); + std::shared_ptr property_val = std::dynamic_pointer_cast( + properties->get().at(property_type).get_ptr()); + + return db_view->get_object(property_val->get_name()); +} + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/effect.h b/libopenage/gamestate/api/effect.h new file mode 100644 index 0000000000..33d1f0fcb6 --- /dev/null +++ b/libopenage/gamestate/api/effect.h @@ -0,0 +1,69 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "gamestate/api/types.h" + + +namespace openage::gamestate::api { + +/** + * Helper class for extracting values from Effect objects in the nyan API. + */ +class APIEffect { +public: + /** + * Check if a nyan object is an Effect (type == \p engine.effect.Effect). + * + * @param obj nyan object. + * + * @return true if the object is an effect, else false. + */ + static bool is_effect(const nyan::Object &obj); + + /** + * Check if an effect is of a given type. + * + * @param effect \p Effect nyan object (type == \p engine.effect.Effect). + * @param type Effect type. + * + * @return true if the effect is of the given type, else false. + */ + static bool check_type(const nyan::Object &effect, + const effect_t &type); + + /** + * Check if an effect has a given property. + * + * @param effect \p Effect nyan object (type == \p engine.effect.Effect). + * @param property Property type. + * + * @return true if the effect has the property, else false. + */ + static bool check_property(const nyan::Object &effect, + const effect_property_t &property); + + /** + * Get the type of an effect. + * + * @param effect \p Effect nyan object (type == \p engine.effect.Effect). + * + * @return Type of the effect. + */ + static effect_t get_type(const nyan::Object &effect); + + /** + * Get the nyan object for a property from an effect. + * + * @param effect \p Effect nyan object (type == \p engine.effect.Effect). + * @param property Property type. + * + * @return \p Property nyan object (type == \p engine.effect.property.Property). + */ + static const nyan::Object get_property(const nyan::Object &effect, + const effect_property_t &property); +}; + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/resistance.cpp b/libopenage/gamestate/api/resistance.cpp new file mode 100644 index 0000000000..60a93707e8 --- /dev/null +++ b/libopenage/gamestate/api/resistance.cpp @@ -0,0 +1,57 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "resistance.h" + +#include "gamestate/api/definitions.h" + + +namespace openage::gamestate::api { + +bool APIResistance::is_resistance(const nyan::Object &obj) { + for (auto &parent : obj.get_parents()) { + if (parent == "engine.resistance.Resistance") { + return true; + } + } + + return false; +} + +bool APIResistance::check_effect_type(const nyan::Object &resistance, + const effect_t &type) { + nyan::fqon_t immediate_parent = resistance.get_parents()[0]; + nyan::ValueHolder effect_type = RESISTANCE_DEFS.get(type); + + std::shared_ptr effect_val = std::dynamic_pointer_cast( + effect_type.get_ptr()); + + return effect_val->get_name() == immediate_parent; +} + +bool APIResistance::check_property(const nyan::Object &resistance, + const resistance_property_t &property) { + std::shared_ptr properties = resistance.get("Resistance.properties"); + nyan::ValueHolder property_type = RESISTANCE_PROPERTY_DEFS.get(property); + + return properties->contains(property_type); +} + +effect_t APIResistance::get_effect_type(const nyan::Object &resistance) { + nyan::fqon_t immediate_parent = resistance.get_parents()[0]; + + return RESISTANCE_TYPE_LOOKUP.get(immediate_parent); +} + +const nyan::Object APIResistance::get_property(const nyan::Object &resistance, + const resistance_property_t &property) { + std::shared_ptr properties = resistance.get("Resistance.properties"); + nyan::ValueHolder property_type = RESISTANCE_PROPERTY_DEFS.get(property); + + std::shared_ptr db_view = resistance.get_view(); + std::shared_ptr property_val = std::dynamic_pointer_cast( + properties->get().at(property_type).get_ptr()); + + return db_view->get_object(property_val->get_name()); +} + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/resistance.h b/libopenage/gamestate/api/resistance.h new file mode 100644 index 0000000000..2f5ec70505 --- /dev/null +++ b/libopenage/gamestate/api/resistance.h @@ -0,0 +1,69 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "gamestate/api/types.h" + + +namespace openage::gamestate::api { + +/** + * Helper class for extracting values from Resistance objects in the nyan API. + */ +class APIResistance { +public: + /** + * Check if a nyan object is an Resistance (type == \p engine.resistance.Resistance). + * + * @param obj nyan object. + * + * @return true if the object is an resistance, else false. + */ + static bool is_resistance(const nyan::Object &obj); + + /** + * Check if an resistance matches a given effect type. + * + * @param resistance \p Resistance nyan object (type == \p engine.resistance.Resistance). + * @param type Effect type. + * + * @return true if the resistance is of the given type, else false. + */ + static bool check_effect_type(const nyan::Object &resistance, + const effect_t &type); + + /** + * Check if an resistance has a given property. + * + * @param resistance \p Resistance nyan object (type == \p engine.resistance.Resistance). + * @param property Property type. + * + * @return true if the resistance has the property, else false. + */ + static bool check_property(const nyan::Object &resistance, + const resistance_property_t &property); + + /** + * Get the matching effect type of a resistance. + * + * @param resistance \p Resistance nyan object (type == \p engine.resistance.Resistance). + * + * @return Type of the resistance. + */ + static effect_t get_effect_type(const nyan::Object &resistance); + + /** + * Get the nyan object for a property from an resistance. + * + * @param resistance \p Resistance nyan object (type == \p engine.resistance.Resistance). + * @param property Property type. + * + * @return \p Property nyan object (type == \p engine.resistance.property.Property). + */ + static const nyan::Object get_property(const nyan::Object &resistance, + const resistance_property_t &property); +}; + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/types.h b/libopenage/gamestate/api/types.h index 2c44fe1e81..7b218afd47 100644 --- a/libopenage/gamestate/api/types.h +++ b/libopenage/gamestate/api/types.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -9,14 +9,41 @@ namespace openage::gamestate::api { * Types of abilities for API objects. */ enum class ability_t { + ACTIVITY, + APPLY_CONTINUOUS_EFFECT, + APPLY_DISCRETE_EFFECT, IDLE, + LINE_OF_SIGHT, LIVE, MOVE, + RANGED_CONTINUOUS_EFFECT, + RANGED_DISCRETE_EFFECT, + RESISTANCE, + SELECTABLE, TURN, // TODO }; +/** + * Types of effects for API objects. + */ +enum class effect_t { + CONTINUOUS_FLAC_DECREASE, + CONTINUOUS_FLAC_INCREASE, + CONTINUOUS_LURE, + CONTINUOUS_TRAC_DECREASE, + CONTINUOUS_TRAC_INCREASE, + CONTINUOUS_TRPC_DECREASE, + CONTINUOUS_TRPC_INCREASE, + + DISCRETE_CONVERT, + DISCRETE_FLAC_DECREASE, + DISCRETE_FLAC_INCREASE, + DISCRETE_MAKE_HARVESTABLE, + DISCRETE_SEND_TO_CONTAINER, +}; + /** * Types of properties for API abilities. */ @@ -29,6 +56,24 @@ enum class ability_property_t { LOCK, }; +/** + * Types of properties for API effects. + */ +enum class effect_property_t { + AREA, + COST, + DIPLOMATIC, + PRIORITY, +}; + +/** + * Types of properties for API resistances. + */ +enum class resistance_property_t { + COST, + STACKED, +}; + /** * Types of properties for API patches. */ @@ -36,4 +81,11 @@ enum class patch_property_t { DIPLOMATIC, }; +/** + * Types of conditions for the XORSwitchGate API activity node. + */ +enum class switch_condition_t { + NEXT_COMMAND, +}; + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/component/api/CMakeLists.txt b/libopenage/gamestate/component/api/CMakeLists.txt index 8588909bf2..dcd5c9ba2f 100644 --- a/libopenage/gamestate/component/api/CMakeLists.txt +++ b/libopenage/gamestate/component/api/CMakeLists.txt @@ -1,7 +1,10 @@ add_sources(libopenage + apply_effect.cpp idle.cpp + line_of_sight.cpp live.cpp move.cpp + resistance.cpp selectable.cpp turn.cpp ) diff --git a/libopenage/gamestate/component/api/apply_effect.cpp b/libopenage/gamestate/component/api/apply_effect.cpp new file mode 100644 index 0000000000..5134b909a8 --- /dev/null +++ b/libopenage/gamestate/component/api/apply_effect.cpp @@ -0,0 +1,45 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "apply_effect.h" + + +namespace openage::gamestate::component { + +ApplyEffect::ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + const time::time_t &creation_time, + bool enabled) : + APIComponent{loop, ability, creation_time, enabled}, + init_time{loop, 0}, + last_used{loop, 0} { +} + +ApplyEffect::ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + bool enabled) : + APIComponent{loop, ability, enabled}, + init_time{loop, 0}, + last_used{loop, 0} { +} + +component_t ApplyEffect::get_type() const { + return component_t::APPLY_EFFECT; +} + +const curve::Discrete &ApplyEffect::get_init_time() const { + return this->init_time; +} + +const curve::Discrete &ApplyEffect::get_last_used() const { + return this->last_used; +} + +void ApplyEffect::set_init_time(const time::time_t &time) { + this->init_time.set_last(time, time); +} + +void ApplyEffect::set_last_used(const time::time_t &time) { + this->last_used.set_last(time, time); +} + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/apply_effect.h b/libopenage/gamestate/component/api/apply_effect.h new file mode 100644 index 0000000000..0411a5abc2 --- /dev/null +++ b/libopenage/gamestate/component/api/apply_effect.h @@ -0,0 +1,85 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "curve/discrete.h" +#include "gamestate/component/api_component.h" +#include "gamestate/component/types.h" +#include "time/time.h" + + +namespace openage::gamestate::component { + +/** + * Component for ApplyEffect abilities. + */ +class ApplyEffect final : public APIComponent { +public: + ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + const time::time_t &creation_time, + bool enabled = true); + + ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + bool enabled = true); + + component_t get_type() const override; + + /** + * Get the last initiaton time that is before the given \p time. + * + * @param time Current simulation time. + * + * @return Curve with the last initiation times. + */ + const curve::Discrete &get_init_time() const; + + /** + * Get the last time the effects were applied before the given \p time. + * + * @param time Current simulation time. + * + * @return Curve with the last application times. + */ + const curve::Discrete &get_last_used() const; + + /** + * Record the simulation time when the entity starts using the ability. + * + * @param time Time at which the entity initiates using the ability. + */ + void set_init_time(const time::time_t &time); + + /** + * Record the simulation time when the entity last applied the effects. + * + * @param time Time at which the entity last applied the effects. + */ + void set_last_used(const time::time_t &time); + +private: + /** + * Simulation time when the entity starts using the corresponding ability + * of the component. For example, when a unit starts attacking. + * + * Effects are applied after \p init_time + \p application_delay (from the nyan object). + * + * The curve stores the time both as the keyframe time AND the keyframe value. In + * practice, only the value should be used. + */ + curve::Discrete init_time; + + /** + * Simulation time when the effects were applied last. + * + * Effects can only be applied again after a cooldown has passed, i.e. + * at \p last_used + \p reload_time (from the nyan object). + * + * The curve stores the time both as the keyframe time AND the keyframe value. In + * practice, only the value should be used. + */ + curve::Discrete last_used; +}; + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/line_of_sight.cpp b/libopenage/gamestate/component/api/line_of_sight.cpp new file mode 100644 index 0000000000..5960a65951 --- /dev/null +++ b/libopenage/gamestate/component/api/line_of_sight.cpp @@ -0,0 +1,12 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "line_of_sight.h" + + +namespace openage::gamestate::component { + +component_t LineOfSight::get_type() const { + return component_t::LINE_OF_SIGHT; +} + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/line_of_sight.h b/libopenage/gamestate/component/api/line_of_sight.h new file mode 100644 index 0000000000..f2d937ed01 --- /dev/null +++ b/libopenage/gamestate/component/api/line_of_sight.h @@ -0,0 +1,18 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "gamestate/component/api_component.h" +#include "gamestate/component/types.h" + + +namespace openage::gamestate::component { + +class LineOfSight final : public APIComponent { +public: + using APIComponent::APIComponent; + + component_t get_type() const override; +}; + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/live.cpp b/libopenage/gamestate/component/api/live.cpp index 01fafde5ea..202a56b664 100644 --- a/libopenage/gamestate/component/api/live.cpp +++ b/libopenage/gamestate/component/api/live.cpp @@ -1,12 +1,10 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #include "live.h" -#include - -#include "curve/discrete.h" #include "curve/iterator.h" #include "curve/map_filter_iterator.h" +#include "curve/segmented.h" #include "gamestate/component/types.h" @@ -18,20 +16,22 @@ component_t Live::get_type() const { void Live::add_attribute(const time::time_t &time, const nyan::fqon_t &attribute, - std::shared_ptr> starting_values) { - this->attribute_values.insert(time, attribute, starting_values); + std::shared_ptr> starting_values) { + this->attributes.insert(time, attribute, starting_values); } void Live::set_attribute(const time::time_t &time, const nyan::fqon_t &attribute, - int64_t value) { - auto attribute_value = this->attribute_values.at(time, attribute); - - if (attribute_value) { - (**attribute_value)->set_last(time, value); + attribute_value_t value) { + auto attribute_iterator = this->attributes.at(time, attribute); + if (attribute_iterator) { + auto attribute_curve = **attribute_iterator; + auto current_value = attribute_curve->get(time); + attribute_curve->set_last_jump(time, current_value, value); } else { - // TODO: fail here + throw Error(MSG(err) << "Attribute not found: " << attribute); } } + } // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/live.h b/libopenage/gamestate/component/api/live.h index 2e1f5e41d5..e8638b7a39 100644 --- a/libopenage/gamestate/component/api/live.h +++ b/libopenage/gamestate/component/api/live.h @@ -13,7 +13,14 @@ #include "time/time.h" -namespace openage::gamestate::component { +namespace openage { + +namespace curve { +template +class Segmented; +} // namespace curve + +namespace gamestate::component { class Live final : public APIComponent { public: using APIComponent::APIComponent; @@ -29,7 +36,7 @@ class Live final : public APIComponent { */ void add_attribute(const time::time_t &time, const nyan::fqon_t &attribute, - std::shared_ptr> starting_values); + std::shared_ptr> starting_values); /** * Set the value of an attribute at a given time. @@ -40,16 +47,17 @@ class Live final : public APIComponent { */ void set_attribute(const time::time_t &time, const nyan::fqon_t &attribute, - int64_t value); + attribute_value_t value); private: using attribute_storage_t = curve::UnorderedMap>>; + std::shared_ptr>>; /** * Map of attribute values by attribute type. */ - attribute_storage_t attribute_values; + attribute_storage_t attributes; }; -} // namespace openage::gamestate::component +} // namespace gamestate::component +} // namespace openage diff --git a/libopenage/gamestate/component/api/resistance.cpp b/libopenage/gamestate/component/api/resistance.cpp new file mode 100644 index 0000000000..9a63b601b3 --- /dev/null +++ b/libopenage/gamestate/component/api/resistance.cpp @@ -0,0 +1,12 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "resistance.h" + + +namespace openage::gamestate::component { + +component_t Resistance::get_type() const { + return component_t::RESISTANCE; +} + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/api/resistance.h b/libopenage/gamestate/component/api/resistance.h new file mode 100644 index 0000000000..78dba25f87 --- /dev/null +++ b/libopenage/gamestate/component/api/resistance.h @@ -0,0 +1,18 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "gamestate/component/api_component.h" +#include "gamestate/component/types.h" + + +namespace openage::gamestate::component { + +class Resistance final : public APIComponent { +public: + using APIComponent::APIComponent; + + component_t get_type() const override; +}; + +} // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/internal/commands/CMakeLists.txt b/libopenage/gamestate/component/internal/commands/CMakeLists.txt index 8c6ec147b9..0bb7056083 100644 --- a/libopenage/gamestate/component/internal/commands/CMakeLists.txt +++ b/libopenage/gamestate/component/internal/commands/CMakeLists.txt @@ -1,4 +1,5 @@ add_sources(libopenage + apply_effect.cpp base_command.cpp custom.cpp idle.cpp diff --git a/libopenage/gamestate/component/internal/commands/apply_effect.cpp b/libopenage/gamestate/component/internal/commands/apply_effect.cpp new file mode 100644 index 0000000000..8c10d608a6 --- /dev/null +++ b/libopenage/gamestate/component/internal/commands/apply_effect.cpp @@ -0,0 +1,15 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "apply_effect.h" + + +namespace openage::gamestate::component::command { + +ApplyEffect::ApplyEffect(const gamestate::entity_id_t &target) : + target{target} {} + +const gamestate::entity_id_t &ApplyEffect::get_target() const { + return this->target; +} + +} // namespace openage::gamestate::component::command diff --git a/libopenage/gamestate/component/internal/commands/apply_effect.h b/libopenage/gamestate/component/internal/commands/apply_effect.h new file mode 100644 index 0000000000..e2a653f28f --- /dev/null +++ b/libopenage/gamestate/component/internal/commands/apply_effect.h @@ -0,0 +1,43 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "gamestate/component/internal/commands/base_command.h" +#include "gamestate/component/internal/commands/types.h" +#include "gamestate/types.h" + + +namespace openage::gamestate::component::command { + +/** + * Command for applying effects to a game entity. + */ +class ApplyEffect final : public Command { +public: + /** + * Creates a new apply effect command. + * + * @param target Target game entity ID. + */ + ApplyEffect(const gamestate::entity_id_t &target); + virtual ~ApplyEffect() = default; + + inline command_t get_type() const override { + return command_t::APPLY_EFFECT; + } + + /** + * Get the ID of the game entity targeted by the command. + * + * @return ID of the targeted game entity. + */ + const gamestate::entity_id_t &get_target() const; + +private: + /** + * Target position. + */ + const gamestate::entity_id_t target; +}; + +} // namespace openage::gamestate::component::command diff --git a/libopenage/gamestate/component/internal/commands/custom.cpp b/libopenage/gamestate/component/internal/commands/custom.cpp index 600d19c38b..7780e0b6ed 100644 --- a/libopenage/gamestate/component/internal/commands/custom.cpp +++ b/libopenage/gamestate/component/internal/commands/custom.cpp @@ -1,11 +1,11 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "custom.h" namespace openage::gamestate::component::command { -CustomCommand::CustomCommand(const std::string &id) : +Custom::Custom(const std::string &id) : id{id} {} diff --git a/libopenage/gamestate/component/internal/commands/custom.h b/libopenage/gamestate/component/internal/commands/custom.h index 14b67f80a5..b199a099af 100644 --- a/libopenage/gamestate/component/internal/commands/custom.h +++ b/libopenage/gamestate/component/internal/commands/custom.h @@ -13,15 +13,15 @@ namespace openage::gamestate::component::command { /** * Custom command for everything that is not covered by the other commands. */ -class CustomCommand : public Command { +class Custom final : public Command { public: /** * Create a new custom command. * * @param id Command identifier. */ - CustomCommand(const std::string &id); - virtual ~CustomCommand() = default; + Custom(const std::string &id); + virtual ~Custom() = default; inline command_t get_type() const override { return command_t::CUSTOM; diff --git a/libopenage/gamestate/component/internal/commands/idle.h b/libopenage/gamestate/component/internal/commands/idle.h index 9870ad1ce5..821929cf28 100644 --- a/libopenage/gamestate/component/internal/commands/idle.h +++ b/libopenage/gamestate/component/internal/commands/idle.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -11,10 +11,10 @@ namespace openage::gamestate::component::command { /** * Command for idling (TODO: rename to Stop?). */ -class IdleCommand : public Command { +class Idle final : public Command { public: - IdleCommand() = default; - virtual ~IdleCommand() = default; + Idle() = default; + virtual ~Idle() = default; inline command_t get_type() const override { return command_t::IDLE; diff --git a/libopenage/gamestate/component/internal/commands/move.cpp b/libopenage/gamestate/component/internal/commands/move.cpp index 26242cea4e..adbcd105bf 100644 --- a/libopenage/gamestate/component/internal/commands/move.cpp +++ b/libopenage/gamestate/component/internal/commands/move.cpp @@ -1,14 +1,14 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "move.h" namespace openage::gamestate::component::command { -MoveCommand::MoveCommand(const coord::phys3 &target) : +Move::Move(const coord::phys3 &target) : target{target} {} -const coord::phys3 &MoveCommand::get_target() const { +const coord::phys3 &Move::get_target() const { return this->target; } diff --git a/libopenage/gamestate/component/internal/commands/move.h b/libopenage/gamestate/component/internal/commands/move.h index f550b546d3..eb07ec24e1 100644 --- a/libopenage/gamestate/component/internal/commands/move.h +++ b/libopenage/gamestate/component/internal/commands/move.h @@ -12,15 +12,15 @@ namespace openage::gamestate::component::command { /** * Command for moving to a target position. */ -class MoveCommand : public Command { +class Move final : public Command { public: /** * Creates a new move command. * * @param target Target position coordinates. */ - MoveCommand(const coord::phys3 &target); - virtual ~MoveCommand() = default; + Move(const coord::phys3 &target); + virtual ~Move() = default; inline command_t get_type() const override { return command_t::MOVE; diff --git a/libopenage/gamestate/component/internal/commands/types.h b/libopenage/gamestate/component/internal/commands/types.h index 840f5eff22..6d6fe25095 100644 --- a/libopenage/gamestate/component/internal/commands/types.h +++ b/libopenage/gamestate/component/internal/commands/types.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -14,6 +14,7 @@ enum class command_t { CUSTOM, IDLE, MOVE, + APPLY_EFFECT, }; } // namespace openage::gamestate::component::command diff --git a/libopenage/gamestate/component/types.h b/libopenage/gamestate/component/types.h index 5e87b8ed23..0e117c7471 100644 --- a/libopenage/gamestate/component/types.h +++ b/libopenage/gamestate/component/types.h @@ -1,10 +1,17 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2024 the openage authors. See copying.md for legal info. #pragma once +#include "util/fixed_point.h" + namespace openage::gamestate::component { +/** + * Type for attribute values. + */ +using attribute_value_t = util::FixedPoint; + /** * Types of components. */ @@ -16,11 +23,14 @@ enum class component_t { ACTIVITY, // API + APPLY_EFFECT, + RESISTANCE, IDLE, TURN, MOVE, SELECTABLE, - LIVE + LIVE, + LINE_OF_SIGHT, }; } // namespace openage::gamestate::component diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index f2f489206c..18e40f52ff 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "entity_factory.h" @@ -22,10 +22,14 @@ #include "gamestate/activity/task_system_node.h" #include "gamestate/activity/xor_event_gate.h" #include "gamestate/activity/xor_gate.h" +#include "gamestate/activity/xor_switch_gate.h" #include "gamestate/api/activity.h" +#include "gamestate/component/api/apply_effect.h" #include "gamestate/component/api/idle.h" +#include "gamestate/component/api/line_of_sight.h" #include "gamestate/component/api/live.h" #include "gamestate/component/api/move.h" +#include "gamestate/component/api/resistance.h" #include "gamestate/component/api/selectable.h" #include "gamestate/component/api/turn.h" #include "gamestate/component/internal/activity.h" @@ -121,7 +125,7 @@ std::shared_ptr EntityFactory::add_game_entity(const std::shared_ptr // use the owner's data to initialize the entity // this ensures that only the owner's tech upgrades apply auto db_view = state->get_player(owner_id)->get_db_view(); - init_components(loop, db_view, entity, nyan_entity); + this->init_components(loop, db_view, entity, nyan_entity); if (this->render_factory) { entity->set_render_entity(this->render_factory->add_world_render_entity()); @@ -194,11 +198,12 @@ void EntityFactory::init_components(const std::shared_ptradd_attribute(time::TIME_MIN, attribute.get_name(), - std::make_shared>(loop, - 0, - "", - nullptr, - start_value)); + std::make_shared>( + loop, + 0, + "", + nullptr, + start_value)); } } else if (ability_parent == "engine.ability.type.Activity") { @@ -208,6 +213,24 @@ void EntityFactory::init_components(const std::shared_ptr(loop, ability_obj); entity->add_component(selectable); } + else if (ability_parent == "engine.ability.type.ApplyContinuousEffect" + or ability_parent == "engine.ability.type.ApplyDiscreteEffect" + or ability_parent == "engine.ability.type.RangedContinuousEffect" + or ability_parent == "engine.ability.type.RangedDiscreteEffect") { + auto apply_effect = std::make_shared(loop, ability_obj); + entity->add_component(apply_effect); + } + else if (ability_parent == "engine.ability.type.Resistance") { + auto resistance = std::make_shared(loop, ability_obj); + entity->add_component(resistance); + } + else if (ability_parent == "engine.ability.type.LineOfSight") { + auto line_of_sight = std::make_shared(loop, ability_obj); + entity->add_component(line_of_sight); + } + else { + log::log(SPAM << "Entity has unrecognized ability type: " << ability_parent); + } } if (activity_ability) { @@ -273,6 +296,9 @@ void EntityFactory::init_activity(const std::shared_ptr(node_id); break; + case activity::node_t::XOR_SWITCH_GATE: + node_id_map[node_id] = std::make_shared(node_id); + break; default: throw Error{ERR << "Unknown activity node type of node: " << node.get_name()}; } diff --git a/libopenage/gamestate/event/send_command.cpp b/libopenage/gamestate/event/send_command.cpp index 8530d6340d..e01e7a5e77 100644 --- a/libopenage/gamestate/event/send_command.cpp +++ b/libopenage/gamestate/event/send_command.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "send_command.h" @@ -19,8 +19,8 @@ namespace component { class CommandQueue; namespace command { -class IdleCommand; -class MoveCommand; +class Idle; +class Move; } // namespace command } // namespace component @@ -65,12 +65,12 @@ void SendCommandHandler::invoke(openage::event::EventLoop & /* loop */, switch (command_type) { case component::command::command_t::IDLE: - command_queue->add_command(time, std::make_shared()); + command_queue->add_command(time, std::make_shared()); break; case component::command::command_t::MOVE: command_queue->add_command( time, - std::make_shared( + std::make_shared( params.get("target", coord::phys3{0, 0, 0}))); break; diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 66f6df9be7..4e665edaef 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -11,6 +11,7 @@ #include "coord/phys.h" #include "gamestate/component/internal/activity.h" #include "gamestate/component/internal/command_queue.h" +#include "gamestate/component/internal/commands/apply_effect.h" #include "gamestate/component/internal/ownership.h" #include "gamestate/component/internal/position.h" #include "gamestate/component/types.h" @@ -206,9 +207,18 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, entity->get_component(component::component_t::OWNERSHIP)); entity_owner->set_owner(time, owner_id); + // ASDF: Remove demo code below for applying effects + // add apply effect command to the command queue + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto apply_command = std::make_shared(entity->get_id()); + command_queue->add_command(time, apply_command); + auto activity = std::dynamic_pointer_cast( entity->get_component(component::component_t::ACTIVITY)); activity->init(time); + + // Important: Running the activity system must be done AFTER all components are initialized entity->get_manager()->run_activity_system(time); gstate->add_game_entity(entity); diff --git a/libopenage/gamestate/system/CMakeLists.txt b/libopenage/gamestate/system/CMakeLists.txt index ec2ea97945..025729409a 100644 --- a/libopenage/gamestate/system/CMakeLists.txt +++ b/libopenage/gamestate/system/CMakeLists.txt @@ -1,6 +1,8 @@ add_sources(libopenage activity.cpp + apply_effect.cpp idle.cpp move.cpp + property.cpp types.cpp ) diff --git a/libopenage/gamestate/system/activity.cpp b/libopenage/gamestate/system/activity.cpp index 7ef1d574b4..92819ab556 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -16,9 +16,11 @@ #include "gamestate/activity/types.h" #include "gamestate/activity/xor_event_gate.h" #include "gamestate/activity/xor_gate.h" +#include "gamestate/activity/xor_switch_gate.h" #include "gamestate/component/internal/activity.h" #include "gamestate/component/types.h" #include "gamestate/game_entity.h" +#include "gamestate/system/apply_effect.h" #include "gamestate/system/idle.h" #include "gamestate/system/move.h" #include "util/fixed_point.h" @@ -111,6 +113,15 @@ void Activity::advance(const time::time_t &start_time, event_wait_time = 0; stop = true; } break; + case activity::node_t::XOR_SWITCH_GATE: { + auto node = std::dynamic_pointer_cast(current_node); + auto next_id = node->get_default()->get_id(); + auto key = node->get_lookup_func()(start_time, entity); + if (node->get_lookup_dict().contains(key)) { + next_id = node->get_lookup_dict().at(key)->get_id(); + } + current_node = node->next(next_id); + } break; default: throw Error{ERR << "Unhandled node type for node " << current_node->str()}; } @@ -125,6 +136,9 @@ const time::time_t Activity::handle_subsystem(const time::time_t &start_time, const std::shared_ptr &state, system_id_t system_id) { switch (system_id) { + case system_id_t::APPLY_EFFECT: + return ApplyEffect::apply_effect(entity, state, entity, start_time); + break; case system_id_t::IDLE: return Idle::idle(entity, start_time); break; diff --git a/libopenage/gamestate/system/apply_effect.cpp b/libopenage/gamestate/system/apply_effect.cpp new file mode 100644 index 0000000000..f0f30b6e23 --- /dev/null +++ b/libopenage/gamestate/system/apply_effect.cpp @@ -0,0 +1,171 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "apply_effect.h" + +#include + +#include "error/error.h" + +#include "gamestate/api/effect.h" +#include "gamestate/api/resistance.h" +#include "gamestate/api/types.h" +#include "gamestate/component/api/apply_effect.h" +#include "gamestate/component/api/live.h" +#include "gamestate/component/api/resistance.h" +#include "gamestate/component/types.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" +#include "gamestate/system/property.h" + + +namespace openage::gamestate::system { + +const time::time_t ApplyEffect::apply_effect(const std::shared_ptr &effector, + const std::shared_ptr &state, + const std::shared_ptr &resistor, + const time::time_t &start_time) { + auto effects_component = std::dynamic_pointer_cast( + effector->get_component(component::component_t::APPLY_EFFECT)); + auto effect_ability = effects_component->get_ability(); + auto batches = effect_ability.get_set("ApplyDiscreteEffect.batches"); + + auto resistance_component = std::dynamic_pointer_cast( + resistor->get_component(component::component_t::RESISTANCE)); + auto resistance_ability = resistance_component->get_ability(); + auto resistances_set = resistance_ability.get_set("Resistance.resistances"); + + auto live_component = std::dynamic_pointer_cast( + resistor->get_component(component::component_t::LIVE)); + + // Extract the effects from the ability + std::unordered_map> effects{}; + for (auto &batch : batches) { + std::shared_ptr batch_obj_val = std::dynamic_pointer_cast(batch.get_ptr()); + auto batch_obj = effect_ability.get_view()->get_object(batch_obj_val->get_name()); + auto batch_effects = batch_obj.get_set("EffectBatch.effects"); + + for (auto &batch_effect : batch_effects) { + std::shared_ptr effect_obj_val = std::dynamic_pointer_cast(batch_effect.get_ptr()); + auto effect_obj = effect_ability.get_view()->get_object(effect_obj_val->get_name()); + auto effect_type = api::APIEffect::get_type(effect_obj); + + if (not effects.contains(effect_type)) { + effects.emplace(effect_type, std::vector{}); + } + + effects[effect_type].push_back(effect_obj); + } + } + + // Extract the resistances from the ability + std::unordered_map> resistances{}; + for (auto &resistance : resistances_set) { + std::shared_ptr resistance_obj_val = std::dynamic_pointer_cast(resistance.get_ptr()); + auto resistance_obj = resistance_ability.get_view()->get_object(resistance_obj_val->get_name()); + auto resistance_type = api::APIResistance::get_effect_type(resistance_obj); + + if (not resistances.contains(resistance_type)) { + resistances.emplace(resistance_type, std::vector{}); + } + + resistances[resistance_type].push_back(resistance_obj); + } + + time::time_t total_time = 0; + + // TODO: Check if delay is necessary + auto delay = effect_ability.get_float("ApplyDiscreteEffect.application_delay"); + auto reload_time = effect_ability.get_float("ApplyDiscreteEffect.reload_time"); + total_time += delay + reload_time; + + // Check for matching effects and resistances + for (auto &effect : effects) { + auto effect_type = effect.first; + auto effect_objs = effect.second; + + if (not resistances.contains(effect_type)) { + continue; + } + + auto resistance_objs = resistances[effect_type]; + + switch (effect_type) { + case api::effect_t::DISCRETE_FLAC_DECREASE: + case api::effect_t::DISCRETE_FLAC_INCREASE: { + // TODO: Filter effects by AttributeChangeType + auto attribute_amount = effect_objs[0].get_object("FlatAttributeChange.change_value"); + auto attribute = attribute_amount.get_object("AttributeAmount.type"); + auto applied_value = get_applied_discrete_flac(effect_objs, resistance_objs); + + // Record the time when the effects were applied + effects_component->set_init_time(start_time + delay); + effects_component->set_last_used(start_time + total_time); + + // Apply the effect to the live component + live_component->set_attribute(start_time + delay, attribute.get_name(), applied_value); + } break; + default: + throw Error(MSG(err) << "Effect type not implemented: " << static_cast(effect_type)); + } + } + + // properties + auto ability = effects_component->get_ability(); + handle_animated(effector, ability, start_time); + + return total_time; +} + + +const component::attribute_value_t ApplyEffect::get_applied_discrete_flac(const std::vector &effects, + const std::vector &resistances) { + component::attribute_value_t applied_value = 0; + component::attribute_value_t min_change = component::attribute_value_t::min_value(); + component::attribute_value_t max_change = component::attribute_value_t::max_value(); + + for (auto &effect : effects) { + auto change_amount = effect.get_object("FlatAttributeChange.change_value"); + auto min_change_amount = effect.get_optional("FlatAttributeChange.min_change_value"); + auto max_change_amount = effect.get_optional("max_change_value"); + + // Get value from change amount + // TODO: Ensure that the attribute is the same for all effects + auto change_value = change_amount.get_int("AttributeAmount.amount"); + applied_value += change_value; + + // TODO: The code below creates a clamp range from the lowest min and highest max values. + // This could create some uintended side effects where the clamped range is much larger + // than expected. Maybe this should be defined better. + + // Get min change value + if (min_change_amount) { + component::attribute_value_t min_change_value = (*min_change_amount)->get_int("AttributeAmount.amount"); + min_change = std::min(min_change_value, min_change); + } + + // Get max change value + if (max_change_amount) { + component::attribute_value_t max_change_value = (*max_change_amount)->get_int("AttributeAmount.amount"); + max_change = std::max(max_change_value, max_change); + } + } + + // TODO: Match effects to exactly one resistance to avoid multi resiatance. + // idea: move effect type to Effect object and make Resistance.resistances a dict. + + for (auto &resistance : resistances) { + auto block_amount = resistance.get_object("FlatAttributeChange.block_value"); + + // Get value from block amount + // TODO: Ensure that the attribute is the same attribute used in the effects + auto block_value = block_amount.get_int("AttributeAmount.amount"); + applied_value -= block_value; + } + + // Clamp the applied value + applied_value = std::clamp(applied_value, min_change, max_change); + + return applied_value; +} + +} // namespace openage::gamestate::system diff --git a/libopenage/gamestate/system/apply_effect.h b/libopenage/gamestate/system/apply_effect.h new file mode 100644 index 0000000000..d7b9f02450 --- /dev/null +++ b/libopenage/gamestate/system/apply_effect.h @@ -0,0 +1,60 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + +#include "gamestate/component/types.h" +#include "time/time.h" + + +namespace openage { + +namespace gamestate { +class GameEntity; +class GameState; + +namespace system { + + +class ApplyEffect { +public: + /** + * Apply the effect of an ability to a game entity. + * + * @param effector Game entity applying the effects. + * @param state Game state. + * @param resistor Target entity of the effects. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ + static const time::time_t apply_effect(const std::shared_ptr &effector, + const std::shared_ptr &state, + const std::shared_ptr &resistor, + const time::time_t &start_time); + +private: + /** + * Get the gross applied value for discrete FlatAttributeChange effects. + * + * The gross applied value is calculated as follows: + * + * applied_value = clamp(change_value - block_value, min_change, max_change) + * + * Effects and resistances MUST have the same attribute change type. + * + * @param effects Effects of the effector. + * @param resistances Resistances of the resistor. + * + * @return Gross applied attribute change value. + */ + static const component::attribute_value_t get_applied_discrete_flac(const std::vector &effects, + const std::vector &resistances); +}; + +} // namespace system +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/system/idle.cpp b/libopenage/gamestate/system/idle.cpp index 3db9f5cb5f..256ef7389e 100644 --- a/libopenage/gamestate/system/idle.cpp +++ b/libopenage/gamestate/system/idle.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #include "idle.h" @@ -15,6 +15,7 @@ #include "gamestate/component/api/idle.h" #include "gamestate/component/types.h" #include "gamestate/game_entity.h" +#include "gamestate/system/property.h" namespace openage::gamestate::system { @@ -27,18 +28,10 @@ const time::time_t Idle::idle(const std::shared_ptr &enti auto idle_component = std::dynamic_pointer_cast( entity->get_component(component::component_t::IDLE)); - auto ability = idle_component->get_ability(); - if (api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED)) { - auto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED); - auto animations = api::APIAbilityProperty::get_animations(property); - auto animation_paths = api::APIAnimation::get_animation_paths(animations); - - if (animation_paths.size() > 0) [[likely]] { - entity->render_update(start_time, animation_paths[0]); - } - } - // TODO: play sound + // properties + auto ability = idle_component->get_ability(); + handle_animated(entity, ability, start_time); return time::time_t::from_int(0); } diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 3da45ceab3..d895eea88d 100644 --- a/libopenage/gamestate/system/move.cpp +++ b/libopenage/gamestate/system/move.cpp @@ -26,6 +26,7 @@ #include "gamestate/game_entity.h" #include "gamestate/game_state.h" #include "gamestate/map.h" +#include "gamestate/system/property.h" #include "pathfinding/path.h" #include "pathfinding/pathfinder.h" #include "util/fixed_point.h" @@ -78,7 +79,7 @@ const time::time_t Move::move_command(const std::shared_ptr( entity->get_component(component::component_t::COMMANDQUEUE)); - auto command = std::dynamic_pointer_cast( + auto command = std::dynamic_pointer_cast( command_queue->pop_command(start_time)); if (not command) [[unlikely]] { @@ -171,15 +172,7 @@ const time::time_t Move::move_default(const std::shared_ptrget_ability(); - if (api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED)) { - auto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED); - auto animations = api::APIAbilityProperty::get_animations(property); - auto animation_paths = api::APIAnimation::get_animation_paths(animations); - - if (animation_paths.size() > 0) [[likely]] { - entity->render_update(start_time, animation_paths[0]); - } - } + handle_animated(entity, ability, start_time); return total_time; } diff --git a/libopenage/gamestate/system/property.cpp b/libopenage/gamestate/system/property.cpp new file mode 100644 index 0000000000..339a65c879 --- /dev/null +++ b/libopenage/gamestate/system/property.cpp @@ -0,0 +1,32 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "property.h" + +#include "gamestate/api/ability.h" +#include "gamestate/api/animation.h" +#include "gamestate/api/property.h" +#include "gamestate/game_entity.h" + + +namespace openage::gamestate::system { + +bool handle_animated(const std::shared_ptr &entity, + const nyan::Object &ability, + const time::time_t &start_time) { + bool animated = api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED); + + if (animated) { + auto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED); + auto animations = api::APIAbilityProperty::get_animations(property); + auto animation_paths = api::APIAnimation::get_animation_paths(animations); + + if (animation_paths.size() > 0) [[likely]] { + // TODO: More than one animation path + entity->render_update(start_time, animation_paths[0]); + } + } + + return animated; +} + +} // namespace openage::gamestate::system diff --git a/libopenage/gamestate/system/property.h b/libopenage/gamestate/system/property.h new file mode 100644 index 0000000000..e7c1cb4f99 --- /dev/null +++ b/libopenage/gamestate/system/property.h @@ -0,0 +1,31 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace system { + +/** + * Handle the animated property of an ability. + * + * @param entity Game entity. + * @param ability Ability object. + * @param start_time Start time of the animation. + * + * @return true if the ability has the property, false otherwise. + */ +bool handle_animated(const std::shared_ptr &entity, + const nyan::Object &ability, + const time::time_t &start_time); + +} // namespace system +} // namespace openage::gamestate diff --git a/libopenage/gamestate/system/types.h b/libopenage/gamestate/system/types.h index 1da20da895..9930b47b9b 100644 --- a/libopenage/gamestate/system/types.h +++ b/libopenage/gamestate/system/types.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2024 the openage authors. See copying.md for legal info. #pragma once @@ -11,6 +11,8 @@ namespace openage::gamestate::system { enum class system_id_t { NONE, + APPLY_EFFECT, + IDLE, MOVE_COMMAND, diff --git a/libopenage/main/demo/pong/gamestate.h b/libopenage/main/demo/pong/gamestate.h index f66fbf1614..a56a7d7041 100644 --- a/libopenage/main/demo/pong/gamestate.h +++ b/libopenage/main/demo/pong/gamestate.h @@ -1,4 +1,4 @@ -// Copyright 2019-2023 the openage authors. See copying.md for legal info. +// Copyright 2019-2024 the openage authors. See copying.md for legal info. #pragma once @@ -36,6 +36,10 @@ class PongEvent { PongEvent() : player(0), state(IDLE) {} + bool operator==(const PongEvent &other) const { + return this->player == other.player && this->state == other.state; + } + size_t player; state_e state; }; diff --git a/libopenage/renderer/stages/hud/object.cpp b/libopenage/renderer/stages/hud/object.cpp index 6e23b6663a..2a69cdb91c 100644 --- a/libopenage/renderer/stages/hud/object.cpp +++ b/libopenage/renderer/stages/hud/object.cpp @@ -36,20 +36,17 @@ void HudDragObject::fetch_updates(const time::time_t &time) { return; } - // Get data from render entity - this->drag_start = this->render_entity->get_drag_start(); - // Thread-safe access to curves needs a lock on the render entity's mutex auto read_lock = this->render_entity->get_read_lock(); - this->drag_pos.sync(this->render_entity->get_drag_pos() /* , this->last_update */); + // Get data from render entity + this->drag_start = this->render_entity->get_drag_start(); - // Unlock the render entity mutex - read_lock.unlock(); + this->drag_pos.sync(this->render_entity->get_drag_pos() /* , this->last_update */); // Set self to changed so that world renderer can update the renderable this->changed = true; - this->render_entity->clear_changed_flag(); + this->render_entity->fetch_done(); this->last_update = time; } diff --git a/libopenage/renderer/stages/hud/render_entity.cpp b/libopenage/renderer/stages/hud/render_entity.cpp index 2ff3f75649..b3f65a70a8 100644 --- a/libopenage/renderer/stages/hud/render_entity.cpp +++ b/libopenage/renderer/stages/hud/render_entity.cpp @@ -20,18 +20,15 @@ void DragRenderEntity::update(const coord::input drag_pos, this->drag_pos.set_insert(time, drag_pos); this->last_update = time; + this->fetch_time = time; this->changed = true; } const curve::Continuous &DragRenderEntity::get_drag_pos() { - std::shared_lock lock{this->mutex}; - return this->drag_pos; } const coord::input DragRenderEntity::get_drag_start() { - std::shared_lock lock{this->mutex}; - return this->drag_start; } diff --git a/libopenage/renderer/stages/hud/render_entity.h b/libopenage/renderer/stages/hud/render_entity.h index 9d15204386..2a48e2d25d 100644 --- a/libopenage/renderer/stages/hud/render_entity.h +++ b/libopenage/renderer/stages/hud/render_entity.h @@ -39,9 +39,6 @@ class DragRenderEntity final : public renderer::RenderEntity { /** * Get the position of the dragged corner. * - * Accessing the drag position curve REQUIRES a read lock on the render entity - * (using \p get_read_lock()) to ensure thread safety. - * * @return Coordinates of the dragged corner. */ const curve::Continuous &get_drag_pos(); @@ -49,8 +46,6 @@ class DragRenderEntity final : public renderer::RenderEntity { /** * Get the position of the start corner. * - * Accessing the drag start is thread-safe. - * * @return Coordinates of the start corner. */ const coord::input get_drag_start(); diff --git a/libopenage/renderer/stages/render_entity.cpp b/libopenage/renderer/stages/render_entity.cpp index dc95de9a6d..77ed3ca2fc 100644 --- a/libopenage/renderer/stages/render_entity.cpp +++ b/libopenage/renderer/stages/render_entity.cpp @@ -2,20 +2,17 @@ #include "render_entity.h" -#include - namespace openage::renderer { RenderEntity::RenderEntity() : changed{false}, - last_update{time::time_t::zero()} { + last_update{time::TIME_ZERO}, + fetch_time{time::TIME_MAX} { } -time::time_t RenderEntity::get_update_time() { - std::shared_lock lock{this->mutex}; - - return this->last_update; +time::time_t RenderEntity::get_fetch_time() { + return this->fetch_time; } bool RenderEntity::is_changed() { @@ -24,14 +21,13 @@ bool RenderEntity::is_changed() { return this->changed; } -void RenderEntity::clear_changed_flag() { - std::unique_lock lock{this->mutex}; - +void RenderEntity::fetch_done() { this->changed = false; + this->fetch_time = time::TIME_MAX; } -std::shared_lock RenderEntity::get_read_lock() { - return std::shared_lock{this->mutex}; +std::unique_lock RenderEntity::get_read_lock() { + return std::unique_lock{this->mutex}; } } // namespace openage::renderer diff --git a/libopenage/renderer/stages/render_entity.h b/libopenage/renderer/stages/render_entity.h index f441452968..bd8535c9a5 100644 --- a/libopenage/renderer/stages/render_entity.h +++ b/libopenage/renderer/stages/render_entity.h @@ -15,32 +15,47 @@ namespace openage::renderer { /** * Interface for render entities that allow pushing updates from game simulation * to renderer. + * + * Accessing the render entity from the renderer thread REQUIRES a + * read lock on the render entity (using \p get_read_lock()) to ensure + * thread safety. */ class RenderEntity { public: ~RenderEntity() = default; /** - * Get the time of the last update. + * Get the earliest time for which updates are available. + * + * Render objects should synchronize their state with the render entity + * from this time onwards. * * Accessing the update time is thread-safe. * * @return Time of last update. */ - time::time_t get_update_time(); + time::time_t get_fetch_time(); /** * Check whether the render entity has received new updates from the * gamestate. * + * Accessing the change flag is thread-safe. + * * @return true if updates have been received, else false. */ bool is_changed(); /** - * Clear the update flag by setting it to false. + * Indicate to this entity that its updates have been processed and transfered to the + * render object. + * + * - Clear the update flag by setting it to false. + * - Sets the fetch time to \p time::MAX_TIME. + * + * Accessing this method is thread-safe. */ - void clear_changed_flag(); + void fetch_done(); /** * Get a shared lock for thread-safe reading from the render entity. @@ -49,7 +64,7 @@ class RenderEntity { * * @return Lock for the render entity. */ - std::shared_lock get_read_lock(); + std::unique_lock get_read_lock(); protected: /** @@ -71,10 +86,17 @@ class RenderEntity { bool changed; /** - * Time of the last update call. + * Time of the last update. */ time::time_t last_update; + /** + * Earliest time for which updates have been received. + * + * \p time::TIME_MAX indicates that no updates are available. + */ + time::time_t fetch_time; + /** * Mutex for protecting threaded access. */ diff --git a/libopenage/renderer/stages/terrain/chunk.cpp b/libopenage/renderer/stages/terrain/chunk.cpp index 58a95d2a95..db6b58dab8 100644 --- a/libopenage/renderer/stages/terrain/chunk.cpp +++ b/libopenage/renderer/stages/terrain/chunk.cpp @@ -32,6 +32,9 @@ void TerrainChunk::fetch_updates(const time::time_t & /* time */) { return; } + // Thread-safe access to data needs a lock on the render entity's mutex + auto read_lock = this->render_entity->get_read_lock(); + // Get the terrain data from the render entity auto terrain_size = this->render_entity->get_size(); auto terrain_paths = this->render_entity->get_terrain_paths(); @@ -54,7 +57,7 @@ void TerrainChunk::fetch_updates(const time::time_t & /* time */) { // this->meshes.push_back(new_mesh); // Indicate to the render entity that its updates have been processed. - this->render_entity->clear_changed_flag(); + this->render_entity->fetch_done(); } void TerrainChunk::update_uniforms(const time::time_t &time) { diff --git a/libopenage/renderer/stages/terrain/render_entity.cpp b/libopenage/renderer/stages/terrain/render_entity.cpp index 2316f5dab7..844773e777 100644 --- a/libopenage/renderer/stages/terrain/render_entity.cpp +++ b/libopenage/renderer/stages/terrain/render_entity.cpp @@ -42,6 +42,7 @@ void RenderEntity::update_tile(const util::Vector2s size, // update the last update time this->last_update = time; + this->fetch_time = time; // update the terrain paths this->terrain_paths.insert(terrain_path); @@ -94,7 +95,7 @@ void RenderEntity::update(const util::Vector2s size, this->tiles = tiles; // update the last update time - this->last_update = time; + this->fetch_time = time; // update the terrain paths this->terrain_paths.clear(); @@ -106,26 +107,18 @@ void RenderEntity::update(const util::Vector2s size, } const std::vector RenderEntity::get_vertices() { - std::shared_lock lock{this->mutex}; - return this->vertices; } const RenderEntity::tiles_t RenderEntity::get_tiles() { - std::shared_lock lock{this->mutex}; - return this->tiles; } const std::unordered_set RenderEntity::get_terrain_paths() { - std::shared_lock lock{this->mutex}; - return this->terrain_paths; } const util::Vector2s RenderEntity::get_size() { - std::shared_lock lock{this->mutex}; - return this->size; } diff --git a/libopenage/renderer/stages/terrain/render_entity.h b/libopenage/renderer/stages/terrain/render_entity.h index 0f726a2351..bd0867a6d2 100644 --- a/libopenage/renderer/stages/terrain/render_entity.h +++ b/libopenage/renderer/stages/terrain/render_entity.h @@ -61,8 +61,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the vertices of the terrain. * - * Accessing the terrain vertices is thread-safe. - * * @return Vector of vertex coordinates. */ const std::vector get_vertices(); @@ -70,8 +68,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the tiles of the terrain. * - * Accessing the terrain tiles is thread-safe. - * * @return Terrain tiles. */ const tiles_t get_tiles(); @@ -79,8 +75,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the terrain paths used in the terrain. * - * Accessing the terrain paths is thread-safe. - * * @return Terrain paths. */ const std::unordered_set get_terrain_paths(); @@ -88,9 +82,7 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the number of vertices on each side of the terrain. * - * Accessing the vertices size is thread-safe. - * - * @return Vector with width as first element and height as second element. + * @return Number of vertices on each side (width x height). */ const util::Vector2s get_size(); diff --git a/libopenage/renderer/stages/world/object.cpp b/libopenage/renderer/stages/world/object.cpp index 2396c94ed2..f4a1b239da 100644 --- a/libopenage/renderer/stages/world/object.cpp +++ b/libopenage/renderer/stages/world/object.cpp @@ -61,12 +61,16 @@ void WorldObject::fetch_updates(const time::time_t &time) { return; } - // Get data from render entity - this->ref_id = this->render_entity->get_id(); - // Thread-safe access to curves needs a lock on the render entity's mutex auto read_lock = this->render_entity->get_read_lock(); - this->position.sync(this->render_entity->get_position()); + + // Data syncs need to be done starting from the time of the last + // recorded change. + auto sync_time = this->render_entity->get_fetch_time(); + + // Get data from render entity + this->ref_id = this->render_entity->get_id(); + this->position.sync(this->render_entity->get_position(), sync_time); this->animation_info.sync(this->render_entity->get_animation_path(), std::function(const std::string &)>( [&](const std::string &path) { @@ -79,16 +83,16 @@ void WorldObject::fetch_updates(const time::time_t &time) { } return this->asset_manager->request_animation(path); }), - this->last_update); - this->angle.sync(this->render_entity->get_angle(), this->last_update); - - // Unlock mutex of the render entity - read_lock.unlock(); + sync_time, + true); + this->angle.sync(this->render_entity->get_angle(), sync_time); // Set self to changed so that world renderer can update the renderable this->changed = true; - this->render_entity->clear_changed_flag(); this->last_update = time; + + // Indicate to the render entity that its updates have been processed. + this->render_entity->fetch_done(); } void WorldObject::update_uniforms(const time::time_t &time) { diff --git a/libopenage/renderer/stages/world/render_entity.cpp b/libopenage/renderer/stages/world/render_entity.cpp index 7a8e145741..b3f919cba3 100644 --- a/libopenage/renderer/stages/world/render_entity.cpp +++ b/libopenage/renderer/stages/world/render_entity.cpp @@ -25,6 +25,9 @@ void RenderEntity::update(const uint32_t ref_id, const time::time_t time) { std::unique_lock lock{this->mutex}; + // Sync the data curves using the earliest time of last update and time + auto sync_time = std::min(this->last_update, time); + this->ref_id = ref_id; std::function to_scene3 = [](const coord::phys3 &pos) { return pos.to_scene3(); @@ -33,11 +36,17 @@ void RenderEntity::update(const uint32_t ref_id, std::function([](const coord::phys3 &pos) { return pos.to_scene3(); }), - this->last_update); - this->angle.sync(angle, this->last_update); + sync_time); + this->angle.sync(angle, sync_time); this->animation_path.set_last(time, animation_path); - this->changed = true; + + // Record time of last update this->last_update = time; + + // Record when the render update should fetch data from the entity + this->fetch_time = std::min(this->fetch_time, time); + + this->changed = true; } void RenderEntity::update(const uint32_t ref_id, @@ -49,31 +58,26 @@ void RenderEntity::update(const uint32_t ref_id, this->ref_id = ref_id; this->position.set_last(time, position.to_scene3()); this->animation_path.set_last(time, animation_path); - this->changed = true; + this->last_update = time; + this->fetch_time = std::min(this->fetch_time, time); + + this->changed = true; } uint32_t RenderEntity::get_id() { - std::shared_lock lock{this->mutex}; - return this->ref_id; } const curve::Continuous &RenderEntity::get_position() { - std::shared_lock lock{this->mutex}; - return this->position; } const curve::Segmented &RenderEntity::get_angle() { - std::shared_lock lock{this->mutex}; - return this->angle; } const curve::Discrete &RenderEntity::get_animation_path() { - std::shared_lock lock{this->mutex}; - return this->animation_path; } diff --git a/libopenage/renderer/stages/world/render_entity.h b/libopenage/renderer/stages/world/render_entity.h index ed9011b8a4..bb467e7bc2 100644 --- a/libopenage/renderer/stages/world/render_entity.h +++ b/libopenage/renderer/stages/world/render_entity.h @@ -60,8 +60,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the ID of the corresponding game entity. * - * Accessing the game entity ID is thread-safe. - * * @return Game entity ID. */ uint32_t get_id(); @@ -69,9 +67,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the position of the entity inside the game world. * - * Accessing the position curve REQUIRES a read lock on the render entity - * (using \p get_read_lock()) to ensure thread safety. - * * @return Position curve of the entity. */ const curve::Continuous &get_position(); @@ -79,9 +74,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the angle of the entity inside the game world. * - * Accessing the angle curve REQUIRES a read lock on the render entity - * (using \p get_read_lock()) to ensure thread safety. - * * @return Angle curve of the entity. */ const curve::Segmented &get_angle(); @@ -89,9 +81,6 @@ class RenderEntity final : public renderer::RenderEntity { /** * Get the animation definition path. * - * Accessing the animation path curve requires a read lock on the render entity - * (using \p get_read_lock()) to ensure thread safety. - * * @return Path to the animation definition file. */ const curve::Discrete &get_animation_path(); diff --git a/openage/convert/processor/conversion/aoc/pregen_processor.py b/openage/convert/processor/conversion/aoc/pregen_processor.py index 48eb8b97e7..59183b2d0f 100644 --- a/openage/convert/processor/conversion/aoc/pregen_processor.py +++ b/openage/convert/processor/conversion/aoc/pregen_processor.py @@ -93,11 +93,14 @@ def generate_activities( ability_parent = "engine.util.activity.node.type.Ability" xor_parent = "engine.util.activity.node.type.XORGate" xor_event_parent = "engine.util.activity.node.type.XOREventGate" + xor_switch_parent = "engine.util.activity.node.type.XORSwitchGate" # Condition types condition_parent = "engine.util.activity.condition.Condition" condition_queue_parent = "engine.util.activity.condition.type.CommandInQueue" - condition_next_move_parent = "engine.util.activity.condition.type.NextCommandMove" + condition_next_command_parent = ( + "engine.util.activity.switch_condition.type.NextCommand" + ) # ======================================================================= # Default (Start -> Ability(Idle) -> End) @@ -274,38 +277,62 @@ def generate_activities( branch_raw_api_object = RawAPIObject(branch_ref_in_modpack, "BranchCommand", api_objects) branch_raw_api_object.set_location(unit_forward_ref) - branch_raw_api_object.add_raw_parent(xor_parent) + branch_raw_api_object.add_raw_parent(xor_switch_parent) - condition_forward_ref = ForwardRef(pregen_converter_group, - "util.activity.types.Unit.NextCommandMove") - branch_raw_api_object.add_raw_member("next", - [condition_forward_ref], - xor_parent) + switch_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.NextCommandSwitch") + branch_raw_api_object.add_raw_member("switch", + switch_forward_ref, + xor_switch_parent) idle_forward_ref = ForwardRef(pregen_converter_group, "util.activity.types.Unit.Idle") branch_raw_api_object.add_raw_member("default", idle_forward_ref, - xor_parent) + xor_switch_parent) pregen_converter_group.add_raw_api_object(branch_raw_api_object) pregen_nyan_objects.update({branch_ref_in_modpack: branch_raw_api_object}) - # condition for branching to move - condition_ref_in_modpack = "util.activity.types.Unit.NextCommandMove" + # condition for branching based on command + condition_ref_in_modpack = "util.activity.types.Unit.NextCommandSwitch" condition_raw_api_object = RawAPIObject(condition_ref_in_modpack, - "NextCommandMove", api_objects) + "NextCommandSwitch", api_objects) condition_raw_api_object.set_location(branch_forward_ref) - condition_raw_api_object.add_raw_parent(condition_next_move_parent) + condition_raw_api_object.add_raw_parent(condition_next_command_parent) + apply_effect_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.ApplyEffect") move_forward_ref = ForwardRef(pregen_converter_group, "util.activity.types.Unit.Move") + next_nodes_lookup = { + api_objects["engine.util.command.type.ApplyEffect"]: apply_effect_forward_ref, + api_objects["engine.util.command.type.Move"]: move_forward_ref, + } condition_raw_api_object.add_raw_member("next", - move_forward_ref, - condition_parent) + next_nodes_lookup, + condition_next_command_parent) pregen_converter_group.add_raw_api_object(condition_raw_api_object) pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object}) + # Apply effect + apply_effect_ref_in_modpack = "util.activity.types.Unit.ApplyEffect" + apply_effect_raw_api_object = RawAPIObject(apply_effect_ref_in_modpack, + "ApplyEffect", api_objects) + apply_effect_raw_api_object.set_location(unit_forward_ref) + apply_effect_raw_api_object.add_raw_parent(ability_parent) + + wait_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Wait") + apply_effect_raw_api_object.add_raw_member("next", wait_forward_ref, + ability_parent) + apply_effect_raw_api_object.add_raw_member("ability", + api_objects["engine.ability.type.ApplyDiscreteEffect"], + ability_parent) + + pregen_converter_group.add_raw_api_object(apply_effect_raw_api_object) + pregen_nyan_objects.update({apply_effect_ref_in_modpack: apply_effect_raw_api_object}) + # Move move_ref_in_modpack = "util.activity.types.Unit.Move" move_raw_api_object = RawAPIObject(move_ref_in_modpack, @@ -355,7 +382,7 @@ def generate_activities( pregen_converter_group.add_raw_api_object(end_raw_api_object) pregen_nyan_objects.update({end_ref_in_modpack: end_raw_api_object}) - @staticmethod + @ staticmethod def generate_attributes( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -468,7 +495,7 @@ def generate_attributes( pregen_converter_group.add_raw_api_object(faith_abbrv_value) pregen_nyan_objects.update({faith_abbrv_ref_in_modpack: faith_abbrv_value}) - @staticmethod + @ staticmethod def generate_diplomatic_stances( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -543,7 +570,7 @@ def generate_diplomatic_stances( pregen_converter_group.add_raw_api_object(gaia_raw_api_object) pregen_nyan_objects.update({gaia_ref_in_modpack: gaia_raw_api_object}) - @staticmethod + @ staticmethod def generate_team_property( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -582,7 +609,7 @@ def generate_team_property( stances, "engine.util.patch.property.type.Diplomatic") - @staticmethod + @ staticmethod def generate_entity_types( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -709,7 +736,7 @@ def generate_entity_types( pregen_converter_group.add_raw_api_object(new_game_entity_type) pregen_nyan_objects.update({class_obj_name: new_game_entity_type}) - @staticmethod + @ staticmethod def generate_effect_types( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -855,7 +882,7 @@ def generate_effect_types( pregen_converter_group.add_raw_api_object(type_raw_api_object) pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object}) - @staticmethod + @ staticmethod def generate_exchange_objects( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1148,7 +1175,7 @@ def generate_exchange_objects( pregen_converter_group.add_raw_api_object(price_mode_raw_api_object) pregen_nyan_objects.update({price_mode_ref_in_modpack: price_mode_raw_api_object}) - @staticmethod + @ staticmethod def generate_formation_types( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1361,7 +1388,7 @@ def generate_formation_types( pregen_converter_group.add_raw_api_object(subformation_raw_api_object) pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object}) - @staticmethod + @ staticmethod def generate_language_objects( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1402,7 +1429,7 @@ def generate_language_objects( pregen_converter_group.add_raw_api_object(language_raw_api_object) pregen_nyan_objects.update({language_ref_in_modpack: language_raw_api_object}) - @staticmethod + @ staticmethod def generate_misc_effect_objects( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1724,7 +1751,7 @@ def generate_misc_effect_objects( calc_forward_ref, "engine.resistance.property.type.Stacked") - @staticmethod + @ staticmethod def generate_modifiers( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1880,7 +1907,7 @@ def generate_modifiers( properties, modifier_parent) - @staticmethod + @ staticmethod def generate_terrain_types( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1920,7 +1947,7 @@ def generate_terrain_types( pregen_converter_group.add_raw_api_object(type_raw_api_object) pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object}) - @staticmethod + @ staticmethod def generate_path_types( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -1985,7 +2012,7 @@ def generate_path_types( pregen_converter_group.add_raw_api_object(path_type_raw_api_object) pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object}) - @staticmethod + @ staticmethod def generate_resources( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup @@ -2185,7 +2212,7 @@ def generate_resources( pregen_converter_group.add_raw_api_object(pop_name_value) pregen_nyan_objects.update({pop_name_ref_in_modpack: pop_name_value}) - @staticmethod + @ staticmethod def generate_death_condition( full_data_set: GenieObjectContainer, pregen_converter_group: ConverterObjectGroup diff --git a/openage/convert/service/init/api_export_required.py b/openage/convert/service/init/api_export_required.py index 8bb3a51850..8bb0e303cb 100644 --- a/openage/convert/service/init/api_export_required.py +++ b/openage/convert/service/init/api_export_required.py @@ -16,7 +16,7 @@ from openage.util.fslike.union import UnionPath -CURRENT_API_VERSION = "0.5.0" +CURRENT_API_VERSION = "0.6.0" def api_export_required(asset_dir: UnionPath) -> bool: diff --git a/openage/convert/service/read/nyan_api_loader.py b/openage/convert/service/read/nyan_api_loader.py index 9980b7fbf4..0b8d6c87e7 100644 --- a/openage/convert/service/read/nyan_api_loader.py +++ b/openage/convert/service/read/nyan_api_loader.py @@ -546,6 +546,13 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.util.activity.condition.type.NextCommandApplyEffect + parents = [api_objects["engine.util.activity.condition.Condition"]] + nyan_object = NyanObject("NextCommandApplyEffect", parents) + fqon = "engine.util.activity.condition.type.NextCommandApplyEffect" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.activity.condition.type.NextCommandIdle parents = [api_objects["engine.util.activity.condition.Condition"]] nyan_object = NyanObject("NextCommandIdle", parents) @@ -630,6 +637,27 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.util.activity.node.type.XORSwitchGate + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("XORSwitchGate", parents) + fqon = "engine.util.activity.node.type.XORSwitchGate" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.switch_condition.SwitchCondition + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("SwitchCondition", parents) + fqon = "engine.util.activity.switch_condition.SwitchCondition" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.switch_condition.type.NextCommand + parents = [api_objects["engine.util.activity.switch_condition.SwitchCondition"]] + nyan_object = NyanObject("NextCommand", parents) + fqon = "engine.util.activity.switch_condition.type.NextCommand" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.animation_override.AnimationOverride parents = [api_objects["engine.root.Object"]] nyan_object = NyanObject("AnimationOverride", parents) @@ -728,6 +756,34 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.util.command.Command + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Command", parents) + fqon = "engine.util.command.Command" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.command.type.ApplyEffect + parents = [api_objects["engine.util.command.Command"]] + nyan_object = NyanObject("ApplyEffect", parents) + fqon = "engine.util.command.type.ApplyEffect" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.command.type.Idle + parents = [api_objects["engine.util.command.Command"]] + nyan_object = NyanObject("Idle", parents) + fqon = "engine.util.command.type.Idle" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.command.type.Move + parents = [api_objects["engine.util.command.Command"]] + nyan_object = NyanObject("Move", parents) + fqon = "engine.util.command.type.Move" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.container_type.SendToContainerType parents = [api_objects["engine.root.Object"]] nyan_object = NyanObject("SendToContainerType", parents) @@ -3324,6 +3380,26 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("default", member_type, None, None, 0) api_object.add_member(member) + # engine.util.activity.node.type.XORSwitchGate + api_object = api_objects["engine.util.activity.node.type.XORSwitchGate"] + + member_type = NyanMemberType(api_objects["engine.util.activity.switch_condition.SwitchCondition"]) + member = NyanMember("switch", member_type, None, None, 0) + api_object.add_member(member) + member_type = NyanMemberType(api_objects["engine.util.activity.node.Node"]) + member = NyanMember("default", member_type, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.switch_condition.type.NextCommand + api_object = api_objects["engine.util.activity.switch_condition.type.NextCommand"] + + subtype = NyanMemberType(api_objects["engine.util.command.Command"]) + key_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + value_type = NyanMemberType(api_objects["engine.util.activity.node.Node"]) + member_type = NyanMemberType(MemberType.DICT, (key_type, value_type)) + member = NyanMember("next", member_type, None, None, 0) + api_object.add_member(member) + # engine.util.animation_override.AnimationOverride api_object = api_objects["engine.util.animation_override.AnimationOverride"] diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 8227537697..1b0b18126a 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -104,6 +104,7 @@ def tests_cpp(): yield "openage::curve::tests::container" yield "openage::curve::tests::curve_types" yield "openage::event::tests::eventtrigger" + yield "openage::gamestate::activity::tests::node_types" def demos_cpp():