diff --git a/cfg/converter/games/game_editions.toml b/cfg/converter/games/game_editions.toml
index 24628e8273..368bc6079e 100644
--- a/cfg/converter/games/game_editions.toml
+++ b/cfg/converter/games/game_editions.toml
@@ -38,9 +38,9 @@ expansions = []
]
[AOC.targetmods.aoe2_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "1.0c"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[AOCDEMO]
@@ -63,9 +63,9 @@ expansions = []
blend = ["data/blendomatic.dat"]
[AOCDEMO.targetmods.trial_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "Trial"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[AOK]
@@ -145,9 +145,9 @@ expansions = []
]
[AOE1DE.targetmods.de1_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "1.0a"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[ROR]
@@ -185,9 +185,9 @@ expansions = []
]
[ROR.targetmods.aoe1_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "1.0a"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[HDEDITION]
@@ -229,9 +229,9 @@ expansions = []
]
[HDEDITION.targetmods.hd_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "5.8"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[AOE2DE]
@@ -278,9 +278,9 @@ expansions = []
]
[AOE2DE.targetmods.de2_base]
- version = "0.6.0"
+ version = "0.7.0"
versionstr = "Update 118476+"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
[SWGB]
@@ -320,6 +320,6 @@ expansions = ["SWGB_CC"]
]
[SWGB.targetmods.swgb_base]
- version = "0.5.1"
+ version = "0.6.0"
versionstr = "1.1-gog4"
- min_api_version = "0.5.0"
+ min_api_version = "0.6.0"
diff --git a/doc/code/curves.md b/doc/code/curves.md
index e914f8bcfe..6473df2b6e 100644
--- a/doc/code/curves.md
+++ b/doc/code/curves.md
@@ -18,6 +18,7 @@ Curves are an integral part of openage's event-based game simulation.
1. [Queue](#queue)
2. [Unordered Map](#unordered-map)
3. [Array](#array)
+4. [Compression](#compression)
## Motivation
@@ -133,6 +134,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**
@@ -292,3 +296,28 @@ Modify operations insert values for a specific point in time.
| Method | Description |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| `sync(Curve, t)` | Replace all keyframes from self after time `t` with keyframes from source `Curve` after 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 transferred 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..2116084e98 100644
--- a/doc/code/game_simulation/activity.md
+++ b/doc/code/game_simulation/activity.md
@@ -5,7 +5,10 @@ configurable.
1. [Motivation](#motivation)
2. [Architecture](#architecture)
-3. [Node Types](#node-types)
+3. [Workflow](#workflow)
+ 1. [Initialization](#initialization)
+ 2. [Advancing in the graph](#advancing-in-the-graph)
+4. [Node Types](#node-types)
## Motivation
@@ -32,7 +35,20 @@ and event triggers that indicate which path to take next. By traversing the node
its paths, the game entities actions are determined. The currently visited node in the graph
corresponds to the current action of a unit.
-Activities are reusable, i.e. they are intended to be shared by many game entities Usually,
+Advancement to the next node can be initiated in several ways, depending on the
+[node type](#node-types) of the current node.
+It can happen automatically or be triggered by an event. In the latter case,
+the event is handled by the `GameEntityManager` which calls an activity *system*
+that processes the event to choose the next node.
+
+Advancing in the graph, i.e. visiting nodes and performing actions costs no ingame time. Time
+delays of actions, e.g. for using an game mechanic like movement, are instead handled by
+scheduling and waiting for events at certain nodes in the graph (e.g. `XOR_EVENT_GATE` nodes).
+This means that when running the activity system, the directed edges of the nodes are followed
+until a node that waits for an event is reached. This allows the activity graph to support
+complex action chains that can be executed in sequence.
+
+Activities are reusable, i.e. they are intended to be shared by many game entities. Usually,
all game entities of the same type should share the same behaviour, so they get assigned
the same activity node graph.
@@ -45,14 +61,47 @@ representation. You don't need to know BPMN to understand the activity control f
we explain everything important about the graphs in our documentation. However,
you can use available [BPMN tools](https://bpmn.io/) to draw activity node graphs.
-## Node Types
+Like all game data, activities and node types for game entities are defined via the
+[nyan API](doc/nyan/openage-lib.md).
+
+
+## Workflow
+
+
+
+### Initialization
+When a game entity is spawned, the engine first checks whether entity's `GameEntity` API object
+has an ability `Activity` assigned. If that is the case, the activity graph is loaded from
+the corresponding API objects defining the graph. Most of this step involves creates the
+nodes and connections for the graph as well as mapping the API objects to node actions.
+
+The loaded activity graph is stored in a `Activity` component that is assigned to the game
+entity. At this point, the activity state of the entity is still uninitialized which allows
+the entity or the component to be cached for faster assignment to entities using the same graph.
+To let the entity become active, the `init(..)` method of the Activity component should be
+called after the entity is completely initialized. This sets the activity state to the start
+node of the actvity graph.
+
+### Advancing in the graph
+
+After the game entity is spawned, the `GameEntityManager` is called once to trigger the initial
+behavior of the game entity. This advances the activity state until the first event branch where
+an event is required for further advancement. The `GameEntityManager` now waits for events
+for the entity to further advance in the graph.
+
+A game entity's current activity state is stored in its `Activity` component in form of
+a reference to the current node. Additionally, the components stores the list of events
+the entity currently waits for to advance.
+
+## 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 | Description | Inputs | Outputs |
+| ----------------- | ------------------------- | ------ | ------- |
+| `START` | Start of activity | 0 | 1 |
+| `END` | End of activity | 1 | 0 |
+| `TASK_SYSTEM` | Run built-in system | 1 | 1 |
+| `TASK_CUSTOM` | Run custom function | 1 | 1 |
+| `XOR_EVENT_GATE` | Wait for event and branch | 1 | 1+ |
+| `XOR_GATE` | Branch on condition | 1 | 1+ |
+| `XOR_SWITCH_GATE` | Branch on value | 1 | 1+ |
diff --git a/doc/code/game_simulation/components.md b/doc/code/game_simulation/components.md
deleted file mode 100644
index dc385a368b..0000000000
--- a/doc/code/game_simulation/components.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# Built-in Components
-
-Overview of the built-in game entity components in the game simulation.
-
-1. [Internal](#internal)
- 1. [Activity](#activity)
- 2. [CommandQueue](#commandqueue)
- 3. [Ownership](#ownership)
- 4. [Position](#position)
-2. [API](#api)
- 1. [Idle](#idle)
- 2. [Live](#live)
- 3. [Move](#move)
- 4. [Turn](#turn)
-
-
-## Internal
-
-Internal components do not have a corresponding nyan API object and thus only
-store runtime data.
-
-### Activity
-
-
-
-The `Activity` component stores a reference to the top-level activity for the
-game entity. Essentially, this gives access to the entire activity node graph
-used by the entity.
-
-Additionally, the current activity state is stored on a discrete curve that
-contains the last visited node.
-
-`Activity` also stores the handles of events initiated by the activity system
-for advancing to the next node. Once the next node is visited, these events
-should be canceled via the `cancel_events(..)` method.
-
-
-### CommandQueue
-
-
-
-The `CommandQueue` component stores commands for the game entity in a [queue curve container](/doc/code/curves.md#queue).
-
-Commands in the queue use `Command` class derivatives which specify a command type
-and payload for the command.
-
-
-### Ownership
-
-
-
-The `Ownership` component stores the ID of the player who owns the game entity.
-
-
-### Position
-
-
-
-The `Position` component stores the location and direction of the game entity
-inside the game world.
-
-The 3D position of the game entity is stored on a continuous curve with value type
-`phys3`.
-
-Directions are stored as angles relative to the camera vector using clock-wise
-rotation. Here are some example values for reference to see how that works in
-practice:
-
-| Angle (degrees) | Direction |
-| --------------- | --------------------- |
-| 0 | look towards camera |
-| 90 | look left |
-| 180 | look away from camera |
-| 270 | look right |
-
-Angles are stored on a segmented curve.
-
-## API
-
-API components have a corresponding nyan API object of type `engine.ability.Ability` defined
-in the nyan API. This API object can be retrieved using the `get_ability(..)` method of the
-component.
-
-### Idle
-
-
-
-**nyan API object:** [`engine.ability.type.Idle`](/doc/nyan/api_reference/reference_ability.md#abilitytypeidle)
-
-The `Idle` component represents the ingame "idle" state of the game entity, i.e. when
-it is doing nothing.
-
-The component stores no runtime data.
-
-
-### Live
-
-
-
-**nyan API object:** [`engine.ability.type.Live`](/doc/nyan/api_reference/reference_ability.md#abilitytypelive)
-
-The `Live` component represents the game entity's ability to have attributes (e.g. health).
-
-An attribute's maximum limit is stored in the nyan API object, while
-the game entity's current attribute values are stored in the component
-on a discrete curve.
-
-
-### Move
-
-
-
-**nyan API object:** [`engine.ability.type.Move`](/doc/nyan/api_reference/reference_ability.md#abilitytypemove)
-
-The `Move` component represents the game entity's ability to move in the game world.
-This also allows moving the game entity with move commands.
-
-The component stores no runtime data.
-
-
-### Turn
-
-
-
-**nyan API object:** [`engine.ability.type.Turn`](/doc/nyan/api_reference/reference_ability.md#abilitytypeturn)
-
-The `Turn` component represents the game entity's ability to change directions in the game world.
-Turning is implicitely required for moving but it also works on its own.
-
-The component stores no runtime data.
diff --git a/doc/code/game_simulation/game_entity.md b/doc/code/game_simulation/game_entity.md
index b511a626a9..e4a146d52c 100644
--- a/doc/code/game_simulation/game_entity.md
+++ b/doc/code/game_simulation/game_entity.md
@@ -7,6 +7,7 @@ Game entities represent objects inside the game world.
3. [Component Data Storage](#component-data-storage)
4. [Control Flow](#control-flow)
1. [System](#system)
+ 1. [System Types](#system-types)
2. [Activities](#activities)
3. [Manager](#manager)
@@ -50,8 +51,6 @@ of the specific entity can be accessed via the `GameEntity` object's `get_compon
## Component Data Storage
-For a description of the available components, check the [component reference](components.md).
-

Components are data storage objects for a game entity that also perform the dual role
@@ -88,8 +87,6 @@ make the game logic maintanable and extensible.
### System
-For a description of the available systems, check the [system reference](systems.md).
-
A *system* in openage is basically a function that operates on game entity
components. They are explicitely separated from game entity and component objects
to allow for more flexible implementation. In practice, systems are implemented as static
@@ -108,6 +105,16 @@ Exceptions should only be made for direct subsystems implementing subroutines
or to avoid code redundancies. The reasoning behind this is that dependencies
between systems may quickly become unmanageable.
+#### System Types
+
+| Type | Description |
+| -------------- | ------------------------------------------ |
+| `Activity` | Handle control flow in the activity graph |
+| `ApplyEffect` | Use the `ApplyEffect` ability of an entity |
+| `CommandQueue` | Control the command queue on an entity |
+| `Idle` | Use the `Idle` ability of an entity |
+| `Move` | Use the `Move` ability of an entity |
+
### Activities
@@ -121,19 +128,6 @@ where paths are taken based on the inputs a game entity receives. The architectu
of the activity control flow is described in more detail in the
[activity control flow documentation](activity.md).
-A game entity's current activity state is stored in its `Activity` component. This component
-holds a reference to the activity node graph used by the entity as well as the
-last visited node. This node describes which action/behavioural state the
-game entity currently is in.
-
-Advancement to the next node can be initiated in several ways, depending on the
-[node type](activity.md#node-types) of the current node.
-It can happen automatically or be triggered by an event. In the latter case,
-the event is handled by the `GameEntityManager` which calls an activity *system*
-that processes the event to choose the next node.
-
-
-
### Manager
diff --git a/doc/code/game_simulation/images/component_activity_uml.svg b/doc/code/game_simulation/images/component_activity_uml.svg
deleted file mode 100644
index c05314eeff..0000000000
--- a/doc/code/game_simulation/images/component_activity_uml.svg
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_command_queue_uml.svg b/doc/code/game_simulation/images/component_command_queue_uml.svg
deleted file mode 100644
index cabb92d98b..0000000000
--- a/doc/code/game_simulation/images/component_command_queue_uml.svg
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_idle_uml.svg b/doc/code/game_simulation/images/component_idle_uml.svg
deleted file mode 100644
index 439a7c39af..0000000000
--- a/doc/code/game_simulation/images/component_idle_uml.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_live_uml.svg b/doc/code/game_simulation/images/component_live_uml.svg
deleted file mode 100644
index b7d47a963e..0000000000
--- a/doc/code/game_simulation/images/component_live_uml.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_move_uml.svg b/doc/code/game_simulation/images/component_move_uml.svg
deleted file mode 100644
index 6076927c78..0000000000
--- a/doc/code/game_simulation/images/component_move_uml.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_ownership_uml.svg b/doc/code/game_simulation/images/component_ownership_uml.svg
deleted file mode 100644
index 141bd5c7ef..0000000000
--- a/doc/code/game_simulation/images/component_ownership_uml.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_position_uml.svg b/doc/code/game_simulation/images/component_position_uml.svg
deleted file mode 100644
index 6f090fcb9e..0000000000
--- a/doc/code/game_simulation/images/component_position_uml.svg
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/component_turn_uml.svg b/doc/code/game_simulation/images/component_turn_uml.svg
deleted file mode 100644
index 9f06b9f662..0000000000
--- a/doc/code/game_simulation/images/component_turn_uml.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/system_idle.svg b/doc/code/game_simulation/images/system_idle.svg
deleted file mode 100644
index 3da9289b05..0000000000
--- a/doc/code/game_simulation/images/system_idle.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/images/system_move.svg b/doc/code/game_simulation/images/system_move.svg
deleted file mode 100644
index 46966a4b4f..0000000000
--- a/doc/code/game_simulation/images/system_move.svg
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
diff --git a/doc/code/game_simulation/systems.md b/doc/code/game_simulation/systems.md
deleted file mode 100644
index 61ea2fcf34..0000000000
--- a/doc/code/game_simulation/systems.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Built-in Systems
-
-Overview of the built-in systems in the game simulation.
-
-1. [Idle](#idle)
-2. [Move](#move)
-
-
-## Idle
-
-
-
-Handles idle actions for game entities.
-
-`idle(..)` updates the animation of the game entity. This requires the game
-entity to have the `Idle` component. The function returns a time of 0 since
-no actionsconsuming simulation time are taken.
-
-
-## Move
-
-
-
-Handles movement actions for game entities.
-
-`move_default(..)` moves a game entity to the new position specified in the function
-call. This requires the game entity to have the `Move` and `Turn` components.
-Waypoints for the exact path are fetched from the pathfinder.
-For every straight path between waypoints, the game entity is turned first, then
-moved (same as in *Age of Empires*). If an animation is available for the `Move`
-component, this animation is forwarded as the game entity's active animation to the
-renderer. The function returns the cumulative time of all turn and movement actions
-initiated by this function.
-
-`move_command(..)` processes the payload from a move *command* to call `move_default(..)`
-with the payload parameters.
diff --git a/libopenage/curve/CMakeLists.txt b/libopenage/curve/CMakeLists.txt
index eb52858f43..05082fe4a8 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 de5c14201d..0996f38a8b 100644
--- a/libopenage/curve/base_curve.h
+++ b/libopenage/curve/base_curve.h
@@ -1,7 +1,8 @@
-// Copyright 2017-2025 the openage authors. See copying.md for legal info.
+// Copyright 2017-2024 the openage authors. See copying.md for legal info.
#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,
@@ -74,30 +76,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.
*/
@@ -113,9 +147,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.
@@ -130,11 +168,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.
@@ -199,8 +241,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
@@ -210,6 +254,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;
@@ -217,32 +268,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);
@@ -250,7 +311,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++;
@@ -258,7 +319,7 @@ std::pair BaseCurve::next_frame(const time::time_t &ti
return elem.as_pair();
}
-template
+template
std::string BaseCurve::str() const {
std::stringstream ss;
ss << "Curve[" << this->idstr() << "]{" << std::endl;
@@ -270,7 +331,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) {
@@ -281,9 +342,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);
@@ -294,15 +356,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);
@@ -313,6 +380,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/container/iterator.h b/libopenage/curve/container/iterator.h
index 7a4fb82d6b..dd7c5f29eb 100644
--- a/libopenage/curve/container/iterator.h
+++ b/libopenage/curve/container/iterator.h
@@ -1,7 +1,8 @@
-// Copyright 2017-2025 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 "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/container/map.h b/libopenage/curve/container/map.h
index 4997824a6d..11913db4f6 100644
--- a/libopenage/curve/container/map.h
+++ b/libopenage/curve/container/map.h
@@ -1,4 +1,4 @@
-// Copyright 2017-2025 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/container/element_wrapper.h"
#include "curve/container/map_filter_iterator.h"
#include "time/time.h"
@@ -19,7 +20,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 {
/**
* Data holder. Maps keys to map elements.
@@ -72,14 +73,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 {
@@ -96,7 +97,7 @@ UnorderedMap::at(const time::time_t &time,
}
}
-template
+template
MapFilterIterator>
UnorderedMap::begin(const time::time_t &time) const {
return MapFilterIterator>(
@@ -106,7 +107,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>(
@@ -116,7 +117,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>(
@@ -131,7 +132,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,
@@ -143,7 +144,7 @@ UnorderedMap::insert(const time::time_t &alive,
value);
}
-template
+template
MapFilterIterator>
UnorderedMap::insert(const time::time_t &alive,
const time::time_t &dead,
@@ -158,7 +159,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);
@@ -167,13 +168,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);
@@ -182,13 +183,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/container/map_filter_iterator.h b/libopenage/curve/container/map_filter_iterator.h
index c9afceee88..7fd93cb6e3 100644
--- a/libopenage/curve/container/map_filter_iterator.h
+++ b/libopenage/curve/container/map_filter_iterator.h
@@ -1,7 +1,8 @@
-// Copyright 2017-2025 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/container/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/container/queue.h b/libopenage/curve/container/queue.h
index fb32a53cbb..510fe32bf0 100644
--- a/libopenage/curve/container/queue.h
+++ b/libopenage/curve/container/queue.h
@@ -11,6 +11,7 @@
#include "error/error.h"
+#include "curve/concept.h"
#include "curve/container/element_wrapper.h"
#include "curve/container/iterator.h"
#include "curve/container/queue_filter_iterator.h"
@@ -31,7 +32,7 @@ namespace curve {
* time it will happen.
* This container can be used to store interactions
*/
-template
+template
class Queue : public event::EventEntity {
public:
/**
@@ -69,6 +70,8 @@ class Queue : public event::EventEntity {
*
* Ignores dead elements.
*
+ * Note: Calling this function on an empty queue is undefined behavior.
+ *
* @param time The time to get the element at.
*
* @return Queue element.
@@ -95,6 +98,8 @@ class Queue : public event::EventEntity {
*
* Ignores dead elements.
*
+ * Note: Calling this function on an empty queue is undefined behavior.
+ *
* @param time The time to get the element at.
* @param value Queue element.
*/
@@ -242,7 +247,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;
@@ -266,7 +271,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(),
@@ -281,7 +286,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(),
@@ -307,7 +312,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;
@@ -317,7 +322,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) {
@@ -333,7 +338,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(),
@@ -343,7 +348,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>(
@@ -358,20 +363,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();
@@ -415,7 +420,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);
@@ -426,8 +431,8 @@ void Queue::clear(const time::time_t &time) {
}
// erase all elements alive at t <= time
- while (this->container.at(at).alive() <= time
- and at != this->container.size()) {
+ while (at != this->container.size()
+ and this->container.at(at).alive() <= time) {
if (this->container.at(at).dead() > time) {
this->container[at].set_dead(time);
}
diff --git a/libopenage/curve/container/queue_filter_iterator.h b/libopenage/curve/container/queue_filter_iterator.h
index 6b2fa471f2..a56a5afb44 100644
--- a/libopenage/curve/container/queue_filter_iterator.h
+++ b/libopenage/curve/container/queue_filter_iterator.h
@@ -1,7 +1,8 @@
-// Copyright 2017-2025 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/container/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/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 b9f9b6b00c..959ac36cd6 100644
--- a/libopenage/curve/discrete.h
+++ b/libopenage/curve/discrete.h
@@ -1,4 +1,4 @@
-// Copyright 2017-2025 the openage authors. See copying.md for legal info.
+// Copyright 2017-2024 the openage authors. See copying.md for legal info.
#pragma once
@@ -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