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 + +![Activity Workflow](images/activity_workflow.png) + +### 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 - -![Activity Component UML](images/component_activity_uml.svg) - -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 - -![CommandQueue Component UML](images/component_activity_uml.svg) - -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 - -![Ownership Component UML](images/component_ownership_uml.svg) - -The `Ownership` component stores the ID of the player who owns the game entity. - - -### Position - -![Position Component UML](images/component_position_uml.svg) - -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 - -![Idle Component UML](images/component_idle_uml.svg) - -**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 - -![Live Component UML](images/component_live_uml.svg) - -**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 - -![Move Component UML](images/component_move_uml.svg) - -**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 - -![Turn Component UML](images/component_turn_uml.svg) - -**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). - ![Component class UML](images/component_uml.svg) 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. - -![Activity Workflow](images/activity_workflow.png) - ### 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 @@ - - -NodeActivityActivitystart_activity: Activitynode: curve::Discretescheduled_events: vector<Event>get_start_activity(): Activityget_node(time_t): Nodeset_node(time_t, Node): voidinit(time_t): voidadd_event(Event): voidcancel_events(): void 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 @@ - - -CommandCommandQueuecommand_queue: curve::Queueadd_command(time_t, Command): voidget_queue(): curve::Queue 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 @@ - - -Idle 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 @@ - - -Liveattribute_values: curve::UnorderedMapadd_attribute(time_t, fqon_t, curve::Discrete)set_attribute(time_t, fqon_t, int64_t): void 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 @@ - - -Move 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 @@ - - -Ownershipowner: curve::Discreteset_owner(time_t, ownership_id_t): voidget_owners(): curve::Discrete 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 @@ - - -Positionposition: curve::Continuousangle: curve::Segmentedget_positions(): curve::Continuousget_angles(): curve::Segmentedset_position(time_t, coord::phys3): voidset_angle(time_t, phys_angle_t): void 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 @@ - - -Turn 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 @@ - - -Idleidle(GameEntity, time_t): time_t 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 @@ - - -Movemove_default(GameEntity, phys3, time_t): time_tmove_command(GameEntity, time_t): time_t 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 - -![Idle systems class UML](images/system_idle.svg) - -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 - -![Move systems class UML](images/system_move.svg) - -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::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..6a4504527c 100644 --- a/libopenage/curve/interpolated.h +++ b/libopenage/curve/interpolated.h @@ -1,8 +1,9 @@ -// Copyright 2019-2024 the openage authors. See copying.md for legal info. +// Copyright 2019-2025 the openage authors. See copying.md for legal info. #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,31 @@ 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_frac Fraction of elapsed time between \p before and \p after. + * Must be between 0.0 and 1.0. + * + * @return Interpolated value. + */ + T interpolate(typename KeyframeContainer::elem_ptr before, + typename KeyframeContainer::elem_ptr after, + double elapsed_frac) 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; @@ -46,36 +66,85 @@ T Interpolated::get(const time::time_t &time) const { auto nxt = e; ++nxt; - time::time_t interval = 0; - - auto offset = time - this->container.get(e).time(); + // difference between time and previous keyframe + auto offset = time.abs_diff(this->container.get(e).time()); + // difference between previous keyframe and next keyframe + time::time_duration_t interval = 0; if (nxt != this->container.size()) { - interval = this->container.get(nxt).time() - this->container.get(e).time(); + interval = this->container.get(nxt).time().abs_diff(this->container.get(e).time()); } - // here, offset > interval will never hold. - // otherwise the underlying storage is broken. - // 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(); } else { - // Interpolation between time(now) and time(next) that has elapsed + // here, offset > interval will never hold. + // otherwise the underlying storage is broken. + + // Interpolation between time(now) and time(next) that has elapsed_frac // 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().abs_diff(this->container.get(last_kept).time()); + auto interval = this->container.get(current + 1).time().abs_diff(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_frac) const { + ENSURE(before <= after, "Index of 'before' must be before 'after'"); + ENSURE(elapsed_frac >= 0.0 && elapsed_frac <= 1.0, + "Elapsed fraction must be between 0.0 and 1.0"); + // 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_frac; + return this->container.get(before).val() + diff_value; +} + } // namespace openage::curve diff --git a/libopenage/curve/keyframe.h b/libopenage/curve/keyframe.h index cd2ebf6ced..70d2b34c77 100644 --- a/libopenage/curve/keyframe.h +++ b/libopenage/curve/keyframe.h @@ -1,7 +1,8 @@ -// Copyright 2019-2025 the openage authors. See copying.md for legal info. +// Copyright 2019-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" @@ -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 8434942058..c74c1f28d3 100644 --- a/libopenage/curve/keyframe_container.h +++ b/libopenage/curve/keyframe_container.h @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #pragma once @@ -7,6 +7,7 @@ #include #include +#include "curve/concept.h" #include "curve/keyframe.h" #include "time/time.h" #include "util/fixed_point.h" @@ -15,40 +16,38 @@ namespace openage::curve { /** - * A timely ordered list with several management functions + * A container storing time-value (keyframe) pairs ordered by time. * - * This class manages different time-based management functions for list - * approach that lies underneath. It contains list to be accessed via a - * non-accurate timing functionality, this means, that for getting a value, not - * the exact timestamp has to be known, it will always return the one closest, - * less or equal to the requested one. + * This class has several management functions for modifying and accessing the + * underlying storage. For getting a keyframe value, the exact timestamp does not + * have to be known, it will always return the one closest, less or equal to the + * requested one. **/ -template +template class KeyframeContainer { public: /** - * A element of the curvecontainer. This is especially used to keep track of - * the value-timing. + * Element of the container. Represents a single time-value pair. */ using keyframe_t = Keyframe; /** - * The underlaying container type. + * Underlaying container type. */ using container_t = std::vector; /** - * The index type to access elements in the container + * Index type to access elements in the container */ using elem_ptr = typename container_t::size_type; /** - * The iterator type to access elements in the container + * Iterator type to access elements in the container. */ using iterator = typename container_t::const_iterator; /** - * Create a new container. + * Create a new keyframe container. * * Inserts a default element with value \p T() at \p time = -INF to ensure * that accessing the container always returns an element. @@ -58,9 +57,9 @@ class KeyframeContainer { KeyframeContainer(); /** - * Create a new container. + * Create a new keyframe container. * - * Inserts a default element at \p time = -INF to ensure + * Inserts a default element \p defaultval at \p time = -INF to ensure * that accessing the container always returns an element. * * @param defaultval Value of default element at -INF. @@ -70,105 +69,188 @@ class KeyframeContainer { KeyframeContainer(const T &defaultval); /** - * Return the number of elements in this container. - * One element is always added at -Inf by default, - * so this is usually your_added_elements + 1. + * Get the number of elements in this container. */ size_t size() const; + /** + * Get the value of the keyframe at the given index. + * + * @param idx Index of the keyframe to get. + * + * @return The keyframe at the given index. + */ const keyframe_t &get(const elem_ptr &idx) const { return this->container.at(idx); } /** - * Get the last element in the curve which is at or before the given time. - * (i.e. elem->time <= time). Given a hint where to start the search. + * Get the last element in the container which is at or before the given time. + * (i.e. elem->time <= time). Include a hint where to start the search. + * + * @param time Request time. + * @param hint Index of the approximate element location. + * + * @return Last element with time <= time. */ elem_ptr last(const time::time_t &time, const elem_ptr &hint) const; /** - * Get the last element with elem->time <= time, without a hint where to start - * searching. + * Get the last element in the container which is at or before the given time. + * (i.e. elem->time <= time). * * The usage of this method is discouraged - except if there is absolutely * no chance for you to have a hint (or the container is known to be nearly - * empty) + * empty). + * + * @param time Request time. + * + * @return Last element with time <= time. */ elem_ptr last(const time::time_t &time) const { return this->last(time, this->container.size()); } /** - * Get the last element in the curve which is before the given time. - * (i.e. elem->time < time). Given a hint where to start the search. + * Get the last element in the container which is before the given time. + * (i.e. elem->time < time). Include a hint where to start the search. + * + * @param time Request time. + * @param hint Index of the approximate element location. + * + * @return Last element with time < time. */ elem_ptr last_before(const time::time_t &time, const elem_ptr &hint) const; /** - * Get the last element with elem->time < time, without a hint where to start - * searching. + * Get the last element in the container which is before the given time. + * + * The usage of this method is discouraged - except if there is absolutely + * no chance for you to have a hint (or the container is known to be nearly + * empty). + * + * @param time Request time. + * + * @return Last element with time < time. */ elem_ptr last_before(const time::time_t &time) const { return this->last_before(time, this->container.size()); } /** - * Insert a new element without a hint. + * Insert a new element. The search for the insertion point 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 you really + * do not have the possibility to get a hint. + * + * If there is already a keyframe with identical time in the container, this will + * insert the new keyframe before the old one. + * + * @param keyframe Keyframe to insert. + * + * @return 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 - * the inserter will start to look for a insertion point. If a good hint is + * Insert a new element. + * + * The hint shall give an approximate location, where + * the inserter will start to look for an 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. The search for the insertion point is + * started from the end of the data. + * + * The use of this function is discouraged, use it only, if you really + * do not have the possibility to get a hint. + * + * If there is a keyframe with identical time in the container, 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 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. + * Create and insert a new element. + * + * The hint shall give an approximate location, where + * the inserter will start to look for an 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 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 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 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 it only, if you 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 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), @@ -177,10 +259,19 @@ class KeyframeContainer { /** * Insert a new value at given time, which overwrites element(s) with - * identical time. If `overwrite_all` is false, overwrite the last same-time - * element. If `overwrite_all` is true, overwrite all elements with same-time. - * Provide a insertion hint to abbreviate the search for the + * identical time. Provide a insertion hint to abbreviate the search for the * insertion point. + * + * `overwrite_all` == true -> overwrite all same-time elements. + * `overwrite_all` == false -> overwrite the last of the time-conflict elements. + * + * @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 Location (index) of the inserted element. */ elem_ptr insert_overwrite(const time::time_t &time, const T &value, @@ -191,18 +282,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 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 you 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 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,39 +315,55 @@ 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 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); } /** - * Erase all elements that come after this last valid element. + * Erase all elements after the given element. + * + * @param last_valid Location of the last element to keep. + * + * @return Location (index) of the last element that was kept. */ elem_ptr erase_after(elem_ptr last_valid); /** - * Erase a single element from the curve. - * Returns the element after the deleted one. + * Erase a single element from the container. + * + * @param it Location of the element to erase. + * + * @return Location (index) of the next element after the erased one. */ elem_ptr erase(elem_ptr it); /** - * Erase all elements with given time. - * Variant without hint, starts the search at the end of the container. - * Returns the iterator after the deleted elements. + * Erase all elements with given time. Starts the search at the end of the container. + * + * @param time Time of the elements to erase. + * + * @return Location (index) of the next element after the erased one. */ elem_ptr erase(const time::time_t &time) { return this->erase(time, this->container.size()); } /** - * Erase all element with given time. - * `hint` is an iterator pointing hopefully close to the searched - * elements. + * Erase all element with given time. Include a hint where to start the search. + * + * @param time Time of the elements to erase. + * @param hint Index of the approximate element location. * - * Returns the iterator after the deleted elements. - * Or, if no elements with this time exist, - * the iterator to the first element after the requested time is returned + * @return Location (index) of the next element after the erased one. */ elem_ptr erase(const time::time_t &time, const elem_ptr &hint) { @@ -250,14 +371,14 @@ class KeyframeContainer { } /** - * Obtain an iterator to the first value with the smallest timestamp. + * Get an iterator to the first keyframe in the container. */ iterator begin() const { return this->container.begin(); } /** - * Obtain an iterator to the position after the last value. + * Get an iterator to the end of the container. */ iterator end() const { return this->container.end(); @@ -298,7 +419,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); @@ -327,37 +448,25 @@ 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 this->container.push_back(keyframe_t(time::TIME_MIN, T())); } -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 this->container.push_back(keyframe_t(time::TIME_MIN, defaultval)); } -template +template size_t KeyframeContainer::size() const { return this->container.size(); } -/* - * Select the last element that is <= a given time. - * If there is multiple elements with the same time, return the last of them. - * If there is no element with such time, return the next element before the time. - * - * 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 { @@ -383,15 +492,7 @@ KeyframeContainer::last(const time::time_t &time, } -/* - * Select the last element that is < a given time. - * If there is multiple elements with the same time, return the last of them. - * If there is no element with such time, return the next element before the time. - * - * Intuitively, this function returns the element that comes right before the - * first element that matches the search time. - */ -template +template typename KeyframeContainer::elem_ptr KeyframeContainer::last_before(const time::time_t &time, const KeyframeContainer::elem_ptr &hint) const { @@ -417,10 +518,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) { @@ -443,10 +541,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, @@ -472,11 +567,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) { @@ -492,10 +583,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 @@ -509,10 +597,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); @@ -520,51 +605,52 @@ 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, const time::time_t &start) { // Delete elements after start time - elem_ptr at = this->last_before(start, this->container.size()); + elem_ptr at = this->last_before(start); 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/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 935aa7141d..2afdc2138d 100644 --- a/libopenage/curve/tests/curve_types.cpp +++ b/libopenage/curve/tests/curve_types.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include #include @@ -232,6 +232,70 @@ void curve_types() { TESTEQUALS(c.get(8), 4); } + { + // 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 size_before = c.get_container().size(); + // 1 initial element at -TIME_MIN + // 8 inserted elements + TESTEQUALS(size_before, 9); + + 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); + + // Compress the curve after t = 0 + c.compress(0); + + auto size_after = c.get_container().size(); + // 1 initial element at -TIME_MIN + // 5 inserted elements + TESTEQUALS(size_after, 6); + + 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); + + auto expected_keyframes = std::vector>>{ + {time::TIME_MIN, 0}, + {0, 0}, + {3, 3}, + {5, 3}, + {6, 4}, + {7, 4}, + }; + TESTEQUALS(c.get_container().size(), expected_keyframes.size()); + + auto idx = 0; + for (auto keyframe : c.get_container()) { + TESTEQUALS(keyframe.time(), expected_keyframes[idx].time()); + TESTEQUALS(keyframe.val(), expected_keyframes[idx].val()); + ++idx; + } + } + // Check the discrete type { auto f = std::make_shared(); @@ -257,6 +321,66 @@ void curve_types() { TESTEQUALS(complex.get(10), "Test 10"); } + { + // 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 size_before = c.get_container().size(); + // 1 initial element at -TIME_MIN + // 6 inserted elements + TESTEQUALS(size_before, 7); + + 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); + + // Compress the curve after t = 0 + c.compress(0); + + auto size_after = c.get_container().size(); + // 1 initial element at -TIME_MIN + // 3 inserted elements + TESTEQUALS(size_after, 4); + + 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); + + auto expected_keyframes = std::vector>{ + {time::TIME_MIN, 0}, + {0, 1}, + {1, 3}, + {4, 4}, + }; + TESTEQUALS(c.get_container().size(), expected_keyframes.size()); + + auto idx = 0; + for (auto keyframe : c.get_container()) { + TESTEQUALS(keyframe.time(), expected_keyframes[idx].time()); + TESTEQUALS(keyframe.val(), expected_keyframes[idx].val()); + ++idx; + } + } + // Check the discrete mod type { auto f = std::make_shared(); 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..1e1ef2a811 100644 --- a/libopenage/gamestate/activity/condition/CMakeLists.txt +++ b/libopenage/gamestate/activity/condition/CMakeLists.txt @@ -1,4 +1,7 @@ add_sources(libopenage + ability_usable.cpp command_in_queue.cpp + next_command_switch.cpp next_command.cpp + target_in_range.cpp ) diff --git a/libopenage/gamestate/activity/condition/ability_usable.cpp b/libopenage/gamestate/activity/condition/ability_usable.cpp new file mode 100644 index 0000000000..6fc44a4ac5 --- /dev/null +++ b/libopenage/gamestate/activity/condition/ability_usable.cpp @@ -0,0 +1,24 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "ability_usable.h" + +#include + +#include "gamestate/game_entity.h" +#include "gamestate/api/ability.h" + + +namespace openage::gamestate::activity { + +bool component_enabled(const time::time_t &/* time */, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr &condition) { + auto ability_obj = condition->get("AbilityUsable.ability"); + auto component_type = api::APIAbility::get_component_type(*ability_obj); + + // TODO: Check if the component is enabled at time + return entity->has_component(component_type); +} + +} // namespace diff --git a/libopenage/gamestate/activity/condition/ability_usable.h b/libopenage/gamestate/activity/condition/ability_usable.h new file mode 100644 index 0000000000..fb3161cf0d --- /dev/null +++ b/libopenage/gamestate/activity/condition/ability_usable.h @@ -0,0 +1,37 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace nyan { +class Object; +} + +namespace openage::gamestate { +class GameEntity; +class GameState; + +namespace activity { + +/** + * Check whether the entity has a component enabled matching the ability from the + * given API object. + * + * @param time Time when the condition is checked. + * @param entity Game entity that the activity is assigned to. + * @param condition nyan object for the condition. Used to read the ability reference. + * + * @return true if there is at least one component enabled matching the ability, false otherwise. + */ +bool component_enabled(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr &condition); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/condition/command_in_queue.cpp b/libopenage/gamestate/activity/condition/command_in_queue.cpp index 7e300701f1..8f8db57c7d 100644 --- a/libopenage/gamestate/activity/condition/command_in_queue.cpp +++ b/libopenage/gamestate/activity/condition/command_in_queue.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "next_command.h" @@ -9,11 +9,13 @@ namespace openage::gamestate::activity { bool command_in_queue(const time::time_t &time, - const std::shared_ptr &entity) { + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { auto command_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); - return not command_queue->get_queue().empty(time); + return not command_queue->get_commands().empty(time); } } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/command_in_queue.h b/libopenage/gamestate/activity/condition/command_in_queue.h index 67c7a794cc..c55eec6b55 100644 --- a/libopenage/gamestate/activity/condition/command_in_queue.h +++ b/libopenage/gamestate/activity/condition/command_in_queue.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -8,21 +8,28 @@ #include "time/time.h" +namespace nyan { +class Object; +} + namespace openage::gamestate { class GameEntity; +class GameState; namespace activity { /** - * Condition for command in queue check in the activity system. + * Check whether the entity has a command in its command queue. * * @param time Time when the condition is checked. - * @param entity Game entity. + * @param entity Game entity that the activity is assigned to. * * @return true if there is at least one command in the entity's command queue, false otherwise. */ bool command_in_queue(const time::time_t &time, - const std::shared_ptr &entity); + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */); } // namespace activity } // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/condition/next_command.cpp b/libopenage/gamestate/activity/condition/next_command.cpp index c0b619a783..57ea9e20fa 100644 --- a/libopenage/gamestate/activity/condition/next_command.cpp +++ b/libopenage/gamestate/activity/condition/next_command.cpp @@ -1,37 +1,33 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "next_command.h" +#include + +#include "gamestate/api/definitions.h" #include "gamestate/component/internal/command_queue.h" #include "gamestate/game_entity.h" namespace openage::gamestate::activity { -bool next_command_idle(const time::time_t &time, - const std::shared_ptr &entity) { +bool next_command(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr &condition) { auto command_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); - if (command_queue->get_queue().empty(time)) { + if (command_queue->get_commands().empty(time)) { return false; } - auto command = command_queue->get_queue().front(time); - return command->get_type() == component::command::command_t::MOVE; -} + auto queue_command = command_queue->get_commands().front(time); -bool next_command_move(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 compare_command = condition->get("NextCommand.command"); + auto compare_type = api::COMMAND_LOOKUP.get(compare_command->get_name()); - auto command = command_queue->get_queue().front(time); - return command->get_type() == component::command::command_t::MOVE; + return queue_command->get_type() == compare_type; } } // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/next_command.h b/libopenage/gamestate/activity/condition/next_command.h index 046a18cec6..a1958a9cfe 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-2025 the openage authors. See copying.md for legal info. #pragma once @@ -8,32 +8,31 @@ #include "time/time.h" +namespace nyan { +class Object; +} + namespace openage::gamestate { class GameEntity; +class GameState; namespace activity { /** - * Condition for next command check in the activity system. - * - * @param time Time when the condition is checked. - * @param entity Game entity. + * Check whether the next command in the entity command queue is of a specific type. * - * @return true if the entity has a idle command next in the queue, false otherwise. - */ -bool next_command_idle(const time::time_t &time, - const std::shared_ptr &entity); - -/** - * Condition for next command check in the activity system. + * The command type is parsed from the nyan object \p condition. * * @param time Time when the condition is checked. - * @param entity Game entity. + * @param entity Game entity that the activity is assigned to. + * @param condition nyan object for the condition. Used to read the command type. * - * @return true if the entity has a move command next in the queue, false otherwise. + * @return true if the entity has the command next in the queue, false otherwise. */ -bool next_command_move(const time::time_t &time, - const std::shared_ptr &entity); +bool next_command(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr &condition); } // 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..165ab5604e --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command_switch.cpp @@ -0,0 +1,27 @@ +// Copyright 2024-2025 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, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + if (command_queue->get_commands().empty(time)) { + return static_cast(component::command::command_t::NONE); + } + + auto command = command_queue->get_commands().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..3e49d71c79 --- /dev/null +++ b/libopenage/gamestate/activity/condition/next_command_switch.h @@ -0,0 +1,38 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace nyan { +class Object; +} + +namespace openage::gamestate { +class GameEntity; +class GameState; + +namespace activity { + +/** + * Check the type of the next command in the queue and return its associated key + * value. + * + * The key value is the result of a static_cast of the \p command_t enum value. + * + * @param time Time when the condition is checked. + * @param entity Game entity that the activity is assigned to. + * + * @return Key of the output node. + */ +int next_command_switch(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/condition/target_in_range.cpp b/libopenage/gamestate/activity/condition/target_in_range.cpp new file mode 100644 index 0000000000..b2536d9de3 --- /dev/null +++ b/libopenage/gamestate/activity/condition/target_in_range.cpp @@ -0,0 +1,106 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "target_in_range.h" + +#include + +#include "log/log.h" + +#include "gamestate/api/ability.h" +#include "gamestate/component/api_component.h" +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/component/internal/position.h" +#include "gamestate/component/types.h" +#include "gamestate/game_entity.h" +#include "gamestate/game_state.h" + + +namespace openage::gamestate::activity { + +bool target_in_range(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &state, + const std::shared_ptr &condition) { + log::log(DBG << "Checking TargetInRange condition for entity " << entity->get_id()); + + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto target = command_queue->get_target(time); + + if (std::holds_alternative(target)) { + // No target exists, exit early + log::log(DBG << "Target for entity " << entity->get_id() << " is not set"); + return false; + } + + // Get the component type matching the ability in the condition + auto checked_ability_obj = condition->get("TargetInRange.ability"); + auto component_type = api::APIAbility::get_component_type(*checked_ability_obj); + if (not entity->has_component(component_type)) { + // Entity does not have the component matching the ability, exit early + log::log(DBG << "Entity " << entity->get_id() << " does not have a component matching ability '" + << checked_ability_obj->get_name() << "'"); + return false; + } + + // Get the actual ability used for the range check + // this step is necessary because the ability defined by the condition + // may be abstract, so multiple abilities may be valid + auto component = std::dynamic_pointer_cast( + entity->get_component(component_type)); + auto used_abilty_obj = component->get_ability(); + + // Fetch min/max range from the ability + nyan::value_float_t min_range = 0; + nyan::value_float_t max_range = 0; + if (api::APIAbility::check_property(used_abilty_obj, api::ability_property_t::RANGED)) { + // Get min/max range from the property of the ability + log::log(DBG << "Ability " << used_abilty_obj.get_name() << " has Ranged property"); + + auto range_obj = api::APIAbility::get_property(used_abilty_obj, api::ability_property_t::RANGED); + min_range = range_obj.get_float("Ranged.min_range"); + max_range = range_obj.get_float("Ranged.max_range"); + } + log::log(DBG << "Min/Max range for ability " << used_abilty_obj.get_name() << ": " + << min_range << "/" << max_range); + + // Fetch the current position of the entity + auto position = std::dynamic_pointer_cast( + entity->get_component(component::component_t::POSITION)); + auto current_pos = position->get_positions().get(time); + + if (std::holds_alternative(target)) { + // Target is a position + log::log(DBG << "Target is a position"); + + auto target_pos = std::get(target); + log::log(DBG << "Target position: " << target_pos); + + auto distance = (target_pos - current_pos).length(); + log::log(DBG << "Distance to target position: " << distance); + + return (distance >= min_range and distance <= max_range); + } + + // Target is a game entity + auto target_entity_id = std::get(target); + log::log(DBG << "Target is a game entity with ID " << target_entity_id); + if (not state->has_game_entity(target_entity_id)) { + // Target entity does not exist + log::log(DBG << "Target entity " << target_entity_id << " does not exist in state"); + return false; + } + + auto target_entity = state->get_game_entity(target_entity_id); + auto target_position = std::dynamic_pointer_cast( + target_entity->get_component(component::component_t::POSITION)); + auto target_pos = target_position->get_positions().get(time); + log::log(DBG << "Target entity " << target_entity_id << " position: " << target_pos); + + auto distance = (target_pos - current_pos).length(); + log::log(DBG << "Distance to target entity " << target_entity_id << ": " << distance); + + return (distance >= min_range and distance <= max_range); +} + +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/condition/target_in_range.h b/libopenage/gamestate/activity/condition/target_in_range.h new file mode 100644 index 0000000000..a40c195637 --- /dev/null +++ b/libopenage/gamestate/activity/condition/target_in_range.h @@ -0,0 +1,44 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "time/time.h" + + +namespace nyan { +class Object; +} + +namespace openage::gamestate { +class GameEntity; +class GameState; + +namespace activity { + +/** + * Check whether the current target of the game entity is within range of a specific ability. + * + * The ability type is parsed from the nyan object \p api_object. + * + * For the check to pass, the following subconditions must be met: + * - \p entity has a target. + * - \p entity has a matching component for the ability type. + * - the distance between \p entity position and the target position is less than + * or equal to the range of the ability. + * + * @param time Time when the condition is checked. + * @param entity Game entity that the activity is assigned to. + * @param condition nyan object for the condition. Used to read the ability reference. + * + * @return true if the target is within range of the ability, false otherwise. + */ +bool target_in_range(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &state, + const std::shared_ptr &condition); + +} // namespace activity +} // namespace openage::gamestate diff --git a/libopenage/gamestate/activity/event/command_in_queue.cpp b/libopenage/gamestate/activity/event/command_in_queue.cpp index af57692078..9d4b5211a1 100644 --- a/libopenage/gamestate/activity/event/command_in_queue.cpp +++ b/libopenage/gamestate/activity/event/command_in_queue.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "command_in_queue.h" @@ -26,7 +26,7 @@ std::shared_ptr primer_command_in_queue(const time::time_ params); auto entity_queue = std::dynamic_pointer_cast( entity->get_component(component::component_t::COMMANDQUEUE)); - auto &queue = entity_queue->get_queue(); + auto &queue = const_cast> &>(entity_queue->get_commands()); queue.add_dependent(ev); return ev; diff --git a/libopenage/gamestate/activity/tests.cpp b/libopenage/gamestate/activity/tests.cpp index 1ddad0e7e8..e270edd70b 100644 --- a/libopenage/gamestate/activity/tests.cpp +++ b/libopenage/gamestate/activity/tests.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include #include @@ -154,8 +154,9 @@ const std::shared_ptr activity_flow(const std::shared_ptr(current); auto next_id = node->get_default()->get_id(); for (auto &condition : node->get_conditions()) { - auto condition_func = condition.second; - if (condition_func(0, nullptr)) { + auto condition_obj = condition.second.api_object; + auto condition_func = condition.second.function; + if (condition_func(0, nullptr, nullptr, condition_obj)) { next_id = condition.first; break; } @@ -232,8 +233,10 @@ void activity_demo() { // Conditional branch static size_t counter = 0; - activity::condition_t branch_task1 = [&](const time::time_t & /* time */, - const std::shared_ptr & /* entity */) { + activity::condition_function_t branch_task1 = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { log::log(INFO << "Checking condition (counter < 4): counter=" << counter); if (counter < 4) { log::log(INFO << "Selecting path 1 (back to task node " << task1->get_id() << ")"); @@ -243,14 +246,20 @@ void activity_demo() { } return false; }; - xor_node->add_output(task1, branch_task1); - activity::condition_t branch_event = [&](const time::time_t & /* time */, - const std::shared_ptr & /* entity */) { + xor_node->add_output(task1, + {nullptr, // API object set to nullptr as it's never used by condition func + branch_task1}); + activity::condition_function_t branch_event = [&](const time::time_t & /* time */, + const std::shared_ptr & /* entity */, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { // No check needed here, the event node is always selected log::log(INFO << "Selecting path 2 (to event node " << event_node->get_id() << ")"); return true; }; - xor_node->add_output(event_node, branch_event); + xor_node->add_output(event_node, + {nullptr, // API object set to nullptr as it's never used by condition func + branch_event}); xor_node->set_default(event_node); // event node 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..bdd90ec2b4 --- /dev/null +++ b/libopenage/gamestate/activity/tests/node_types.cpp @@ -0,0 +1,272 @@ +// Copyright 2024-2025 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::IDLE; + 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, + {nullptr, + [](const time::time_t &time, + const std::shared_ptr & /* entity */, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { + return time == time::TIME_ZERO; + }}); + + auto option2_node = std::make_shared(3); + xor_gate_node->add_output(option2_node, + {nullptr, + [](const time::time_t &time, + const std::shared_ptr & /* entity */, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { + return time == time::TIME_MAX; + }}); + + auto conditions = xor_gate_node->get_conditions(); + + // Check the conditions + TESTEQUALS(conditions.size(), 2); + + // Check if the conditions are set correctly + // we don't pass GameEntity, GameState, or nyan::Object as they are not used by the condition functions + TESTEQUALS(conditions.at(2).function(time::TIME_ZERO, nullptr, nullptr, nullptr), true); + TESTEQUALS(conditions.at(3).function(time::TIME_ZERO, nullptr, nullptr, nullptr), false); + + TESTEQUALS(conditions.at(2).function(time::TIME_MAX, nullptr, nullptr, nullptr), false); + TESTEQUALS(conditions.at(3).function(time::TIME_MAX, nullptr, nullptr, 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 */, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { + if (time == time::TIME_ZERO) { + return 1; + } + if (time == time::TIME_MAX) { + return 2; + } + + return 0; + }; + // we don't pass a nyan::Object as it's not used by the switch function + xor_switch_gate_node->set_switch_func({nullptr, lookup_func}); + + // Check the switch function + // we don't pass GameEntity or GameState as it's not used by the switch functions + auto switch_condition = xor_switch_gate_node->get_switch_func(); + auto switch_obj = switch_condition.api_object; + auto switch_func = switch_condition.function; + TESTEQUALS(switch_func(time::TIME_ZERO, nullptr, nullptr, switch_obj), 1); + TESTEQUALS(switch_func(time::TIME_MAX, nullptr, nullptr, switch_obj), 2); + TESTEQUALS(switch_func(time::TIME_MIN, nullptr, nullptr, switch_obj), 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..b0320dbd1b 100644 --- a/libopenage/gamestate/activity/types.h +++ b/libopenage/gamestate/activity/types.h @@ -1,9 +1,29 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once +#include +#include -namespace openage::gamestate::activity { +#include "time/time.h" + + +namespace nyan { +class Object; +} // namespace nyan + +namespace openage { + +namespace event { +class Event; +class EventLoop; +} // namespace event + +namespace gamestate { +class GameEntity; +class GameState; + +namespace activity { /** * Node types in the flow graph. @@ -13,8 +33,91 @@ enum class node_t { END, XOR_EVENT_GATE, XOR_GATE, + XOR_SWITCH_GATE, TASK_CUSTOM, TASK_SYSTEM, }; -} // namespace openage::gamestate::activity +/** + * Create and register an event on the event loop. + * + * When the event is executed, the control flow continues on the branch + * associated with the event. + * + * @param time Time at which the primer function is executed. + * @param entity Game entity that the activity is assigned to. + * @param loop Event loop that events are registered on. + * @param state Game state. + * @param next_id ID of the next node to visit. This is passed as an event parameter. + * + * @return Event registered on the event loop. + */ +using event_primer_t = std::function(const time::time_t &time, + const std::shared_ptr &entity, + const std::shared_ptr &loop, + const std::shared_ptr &state, + size_t next_id)>; + +/** + * Function that determines if an output node is chosen. + * + * @param time Current simulation time. + * @param entity Entity that is executing the activity. + * @param api_object API object that is used to define the condition. May be used to + * store additional data for evaluating the condition. + * + * @return true if the output node is chosen, false otherwise. + */ +using condition_function_t = std::function &entity, + const std::shared_ptr &state, + const std::shared_ptr &api_object)>; + +/** + * Condition used to determine if an output node is chosen. + */ +struct condition { + /// API object for the condition definition. + std::shared_ptr api_object; + /// Checks whether the condition is true. + /// TODO: We could look this function up at runtime. + condition_function_t function; +}; + +/** + * Type used as key for the node lookup dict of the XorSwitchGate. + */ +using switch_key_t = int; + +/** + * Function that retrieves a key for the node lookup dict of the + * XorSwitchGate. The lookup key is calculated from the current state + * of an entity. + * + * @param time Current simulation time. + * @param entity Entity that is executing the activity. + * @param api_object API object that is used to define the condition. May be used to + * store additional data for evaluating the condition. + * + * @return Lookup key. + */ +using switch_function_t = std::function &entity, + const std::shared_ptr &state, + const std::shared_ptr &api_object)>; + +/** + * Condition used to determine which output node of the + * XorSwitchGate is chosen. + */ +struct switch_condition { + /// API object for the condition definition. + std::shared_ptr api_object; + /// Returns the node lookup key for the output node that is chosen. + /// TODO: We could look this function up at runtime. + switch_function_t function; +}; + +} // namespace activity +} // namespace gamestate +} // namespace openage diff --git a/libopenage/gamestate/activity/xor_event_gate.h b/libopenage/gamestate/activity/xor_event_gate.h index 38c8ccacb1..7a3853813a 100644 --- a/libopenage/gamestate/activity/xor_event_gate.h +++ b/libopenage/gamestate/activity/xor_event_gate.h @@ -1,8 +1,7 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once -#include #include #include #include @@ -15,39 +14,7 @@ #include "time/time.h" -namespace openage { -namespace event { -class Event; -class EventLoop; -} // namespace event - -namespace gamestate { -class GameEntity; -class GameState; - -namespace activity { - - -/** - * Create and register an event on the event loop. - * - * When the event is executed, the control flow continues on the branch - * associated with the event. - * - * @param time Time at which the primer function is executed. - * @param entity Game entity that the activity is assigned to. - * @param loop Event loop that events are registered on. - * @param state Game state. - * @param next_id ID of the next node to visit. This is passed as an event parameter. - * - * @return Event registered on the event loop. - */ -using event_primer_t = std::function(const time::time_t &, - const std::shared_ptr &, - const std::shared_ptr &, - const std::shared_ptr &, - size_t next_id)>; - +namespace openage::gamestate::activity { /** * Waits for an event to be executed before continuing the control flow. @@ -61,7 +28,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. @@ -107,6 +74,4 @@ class XorEventGate : public Node { std::map primers; }; -} // namespace activity -} // namespace gamestate -} // namespace openage +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_gate.cpp b/libopenage/gamestate/activity/xor_gate.cpp index 5d37908f8b..937d476ef4 100644 --- a/libopenage/gamestate/activity/xor_gate.cpp +++ b/libopenage/gamestate/activity/xor_gate.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "xor_gate.h" @@ -17,7 +17,7 @@ XorGate::XorGate(node_id_t id, XorGate::XorGate(node_id_t id, node_label_t label, const std::vector> &outputs, - const std::vector &conditions, + const std::vector &conditions, const std::shared_ptr &default_node) : Node{id, label, outputs}, conditions{}, @@ -33,12 +33,12 @@ XorGate::XorGate(node_id_t id, } void XorGate::add_output(const std::shared_ptr &output, - const condition_t condition_func) { + const condition condition) { this->outputs.emplace(output->get_id(), output); - this->conditions.emplace(output->get_id(), condition_func); + this->conditions.emplace(output->get_id(), condition); } -const std::map &XorGate::get_conditions() const { +const std::map &XorGate::get_conditions() const { return this->conditions; } diff --git a/libopenage/gamestate/activity/xor_gate.h b/libopenage/gamestate/activity/xor_gate.h index f73287ed86..89b0816a29 100644 --- a/libopenage/gamestate/activity/xor_gate.h +++ b/libopenage/gamestate/activity/xor_gate.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -16,22 +16,7 @@ #include "time/time.h" -namespace openage::gamestate { -class GameEntity; - -namespace activity { - -/** - * Function that determines if an output node is chosen. - * - * @param time Current game time. - * @param entity Entity that is executing the activity. - * - * @return true if the output node is chosen, false otherwise. - */ -using condition_t = std::function &)>; - +namespace openage::gamestate::activity { /** * Chooses one of its output nodes based on conditions. @@ -59,7 +44,7 @@ class XorGate : public Node { XorGate(node_id_t id, node_label_t label, const std::vector> &outputs, - const std::vector &conditions, + const std::vector &conditions, const std::shared_ptr &default_node); virtual ~XorGate() = default; @@ -72,18 +57,18 @@ class XorGate : public Node { * Add an output node. * * @param output Output node. - * @param condition_func Function that determines whether this output node is chosen. + * @param condition Function that determines whether this output node is chosen. * This must be a valid node ID of one of the output nodes. */ void add_output(const std::shared_ptr &output, - const condition_t condition_func); + const condition condition); /** * Get the output->condition mappings. * * @return Conditions for each output node. */ - const std::map &get_conditions() const; + const std::map &get_conditions() const; /** * Get the default output node. @@ -103,11 +88,11 @@ class XorGate : public Node { private: /** - * Maps output node IDs to condition functions. + * Maps output node IDs to conditions. * * Conditions are checked in order they appear in the map. */ - std::map conditions; + std::map conditions; /** * Default output node. Chosen if no condition is true. @@ -115,5 +100,4 @@ class XorGate : public Node { std::shared_ptr default_node; }; -} // namespace activity -} // namespace openage::gamestate +} // namespace openage::gamestate::activity diff --git a/libopenage/gamestate/activity/xor_switch_gate.cpp b/libopenage/gamestate/activity/xor_switch_gate.cpp new file mode 100644 index 0000000000..70658d3e5c --- /dev/null +++ b/libopenage/gamestate/activity/xor_switch_gate.cpp @@ -0,0 +1,53 @@ +// Copyright 2024-2025 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 switch_condition &switch_func, + const lookup_dict_t &lookup_dict, + const std::shared_ptr &default_node) : + Node{id, label}, + switch_func{switch_func}, + lookup_dict{lookup_dict}, + default_node{default_node} {} + +void XorSwitchGate::set_output(const std::shared_ptr &output, + const switch_key_t &key) { + this->outputs.emplace(output->get_id(), output); + this->lookup_dict.emplace(key, output); +} + +const switch_condition &XorSwitchGate::get_switch_func() const { + return this->switch_func; +} + +void XorSwitchGate::set_switch_func(const switch_condition &switch_func) { + this->switch_func = switch_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..a4e32beb42 --- /dev/null +++ b/libopenage/gamestate/activity/xor_switch_gate.h @@ -0,0 +1,132 @@ +// Copyright 2024-2025 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::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: + /** + * 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 switch_func Function for evaluating the key of the output node. + * @param lookup_dict Initial lookup dict that maps switch keys to output node IDs. + * @param default_node Default output node. Chosen if \p switch_func does not + * return a key in the lookup dict. + */ + XorSwitchGate(node_id_t id, + node_label_t label, + const switch_condition &switch_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 switch key. + * + * @param output Output node. + * @param key Enumeration value. + */ + void set_output(const std::shared_ptr &output, + const switch_key_t &key); + + /** + * Get the switch function for determining the output nodes. + * + * @return Switch function. + */ + const switch_condition &get_switch_func() const; + + /** + * Set the switch function for determining the output nodes. + * + * @param switch_func Switch function. + */ + void set_switch_func(const switch_condition &switch_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. + */ + switch_condition switch_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 openage::gamestate::activity diff --git a/libopenage/gamestate/api/CMakeLists.txt b/libopenage/gamestate/api/CMakeLists.txt index f3ad8d7b40..db07762baf 100644 --- a/libopenage/gamestate/api/CMakeLists.txt +++ b/libopenage/gamestate/api/CMakeLists.txt @@ -3,9 +3,12 @@ add_sources(libopenage activity.cpp animation.cpp definitions.cpp + effect.cpp + object.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..49b9ef4244 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-2025 the openage authors. See copying.md for legal info. #include "ability.h" @@ -11,24 +11,47 @@ #include "datastructure/constexpr_map.h" #include "gamestate/api/definitions.h" +#include "gamestate/api/util.h" 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"; + return obj.extends("engine.ability.Ability"); +} + +bool APIAbility::check_type(const nyan::Object &ability, + const ability_t &type) { + nyan::fqon_t api_parent = get_api_parent(ability); + nyan::ValueHolder ability_type = ABILITY_DEFS.get(type); + + auto ability_val = ability_type.get_value_ptr(); + + return ability_val->get_name() == api_parent; +} + +ability_t APIAbility::get_type(const nyan::Object &ability) { + nyan::fqon_t api_parent = get_api_parent(ability); + + // TODO: remove once other ability types are implemented + if (not ABILITY_TYPE_LOOKUP.contains(api_parent)) { + return ability_t::UNKNOWN; + } + + return ABILITY_TYPE_LOOKUP.get(api_parent); +} + +component::component_t APIAbility::get_component_type(const nyan::Object &ability) { + auto ability_type = APIAbility::get_type(ability); + + return COMPONENT_TYPE_LOOKUP.get(ability_type); } 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); } @@ -37,8 +60,7 @@ const nyan::Object APIAbility::get_property(const nyan::Object &ability, const a nyan::ValueHolder property_type = ABILITY_PROPERTY_DEFS.get(property); std::shared_ptr db_view = ability.get_view(); - std::shared_ptr property_val = std::dynamic_pointer_cast( - properties->get().at(property_type).get_ptr()); + auto property_val = properties->get().at(property_type).get_value_ptr(); return db_view->get_object(property_val->get_name()); } diff --git a/libopenage/gamestate/api/ability.h b/libopenage/gamestate/api/ability.h index d0ba6ea54e..f58974385e 100644 --- a/libopenage/gamestate/api/ability.h +++ b/libopenage/gamestate/api/ability.h @@ -1,10 +1,12 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once #include #include "gamestate/api/types.h" +#include "gamestate/component/types.h" + namespace openage::gamestate::api { @@ -22,6 +24,35 @@ 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); + + /** + * Get the internal ability type from a nyan ability. + * + * @param ability \p Ability nyan object (type == \p engine.ability.Ability). + * + * @return Internal ability type. + */ + static ability_t get_type(const nyan::Object &ability); + + /** + * Get the internal component type from a nyan ability. + * + * @param ability \p Ability nyan object (type == \p engine.ability.Ability). + * + * @return Internal component type. + */ + static component::component_t get_component_type(const nyan::Object &ability); + /** * Check if an ability has a given property. * diff --git a/libopenage/gamestate/api/activity.cpp b/libopenage/gamestate/api/activity.cpp index edd596abe8..b72b2630a6 100644 --- a/libopenage/gamestate/api/activity.cpp +++ b/libopenage/gamestate/api/activity.cpp @@ -1,15 +1,15 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "activity.h" #include "gamestate/api/definitions.h" +#include "gamestate/api/util.h" namespace openage::gamestate::api { bool APIActivity::is_activity(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.activity.Activity"; + return obj.extends("engine.util.activity.Activity"); } nyan::Object APIActivity::get_start(const nyan::Object &activity) { @@ -21,13 +21,13 @@ nyan::Object APIActivity::get_start(const nyan::Object &activity) { bool APIActivityNode::is_node(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.activity.node.Node"; + return obj.extends("engine.util.activity.node.Node"); } activity::node_t APIActivityNode::get_type(const nyan::Object &node) { - nyan::fqon_t immediate_parent = node.get_parents()[0]; - return ACTIVITY_NODE_DEFS.get(immediate_parent); + nyan::fqon_t api_parent = get_api_parent(node); + + return ACTIVITY_NODE_LOOKUP.get(api_parent); } std::vector APIActivityNode::get_next(const nyan::Object &node) { @@ -38,7 +38,20 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { } // 1 next node case activity::node_t::TASK_SYSTEM: { - auto next = node.get("Ability.next"); + auto api_parent = get_api_parent(node); + + nyan::memberid_t member_name; + if (api_parent == "engine.util.activity.node.type.Ability") { + member_name = "Ability.next"; + } + else if (api_parent == "engine.util.activity.node.type.Task") { + member_name = "Task.next"; + } + else { + throw Error(MSG(err) << "Node type '" << api_parent << "' cannot be used to get the next node."); + } + + auto next = node.get(member_name); std::shared_ptr db_view = node.get_view(); return {db_view->get_object(next->get_name())}; } @@ -54,7 +67,7 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { std::vector next_nodes; for (auto &condition : conditions->get()) { - auto condition_value = std::dynamic_pointer_cast(condition.get_ptr()); + auto condition_value = condition.get_value_ptr(); auto condition_obj = db_view->get_object(condition_value->get_name()); auto next_node_value = condition_obj.get("Condition.next"); @@ -72,44 +85,128 @@ std::vector APIActivityNode::get_next(const nyan::Object &node) { std::vector next_nodes; for (auto &next_node : next->get()) { - auto next_node_value = std::dynamic_pointer_cast(next_node.second.get_ptr()); + auto next_node_value = next_node.second.get_value_ptr(); next_nodes.push_back(db_view->get_object(next_node_value->get_name())); } 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 = get_api_parent(switch_condition_obj); + auto switch_condition_type = ACTIVITY_SWITCH_CONDITION_TYPE_LOOKUP.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 = next_node.second.get_value_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."); } } -system::system_id_t APIActivityNode::get_system_id(const nyan::Object &ability_node) { - auto ability = ability_node.get("Ability.ability"); +system::system_id_t APIActivityNode::get_system_id(const nyan::Object &node) { + nyan::fqon_t api_parent = get_api_parent(node); + + nyan::fqon_t task_obj_fqon; + if (api_parent == "engine.util.activity.node.type.Ability") { + task_obj_fqon = node.get("Ability.ability")->get_name(); + } + else if (api_parent == "engine.util.activity.node.type.Task") { + task_obj_fqon = node.get("Task.task")->get_name(); + } + else { + throw Error(MSG(err) << "Node type '" << api_parent << "' cannot be used to get the system ID."); + } + + // Get the API parent of the task object to look up the system ID + auto view = node.get_view(); + auto task_obj = view->get_object(task_obj_fqon); + task_obj_fqon = get_api_parent(task_obj); - if (not ACTIVITY_TASK_SYSTEM_DEFS.contains(ability->get_name())) [[unlikely]] { - throw Error(MSG(err) << "Ability '" << ability->get_name() << "' has no associated system defined."); + if (not ACTIVITY_TASK_SYSTEM_LOOKUP.contains(task_obj_fqon)) [[unlikely]] { + throw Error(MSG(err) << "'" << task_obj.get_name() << "' has no associated system defined."); } - return ACTIVITY_TASK_SYSTEM_DEFS.get(ability->get_name()); + return ACTIVITY_TASK_SYSTEM_LOOKUP.get(task_obj_fqon); } bool APIActivityCondition::is_condition(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.activity.condition.Condition"; + nyan::fqon_t api_parent = get_api_parent(obj); + + return api_parent == "engine.util.activity.condition.Condition"; } -activity::condition_t APIActivityCondition::get_condition(const nyan::Object &condition) { - nyan::fqon_t immediate_parent = condition.get_parents()[0]; - return ACTIVITY_CONDITIONS.get(immediate_parent); +activity::condition_function_t APIActivityCondition::get_condition(const nyan::Object &condition) { + nyan::fqon_t api_parent = get_api_parent(condition); + + return ACTIVITY_CONDITION_LOOKUP.get(api_parent); +} + +bool APIActivitySwitchCondition::is_switch_condition(const nyan::Object &obj) { + nyan::fqon_t api_parent = get_api_parent(obj); + + return api_parent == "engine.util.activity.switch_condition.SwitchCondition"; +} + +activity::switch_function_t APIActivitySwitchCondition::get_switch_func(const nyan::Object &condition) { + nyan::fqon_t api_parent = get_api_parent(condition); + + return ACTIVITY_SWITCH_CONDITION_LOOKUP.get(api_parent); +} + +APIActivitySwitchCondition::lookup_map_t APIActivitySwitchCondition::get_lookup_map(const nyan::Object &condition) { + nyan::fqon_t api_parent = get_api_parent(condition); + + auto switch_condition_type = ACTIVITY_SWITCH_CONDITION_TYPE_LOOKUP.get(api_parent); + + switch (switch_condition_type) { + case switch_condition_t::NEXT_COMMAND: { + auto next = condition.get("NextCommand.next"); + lookup_map_t lookup_map{}; + for (auto next_node : next->get()) { + auto key_value = next_node.first.get_value_ptr(); + auto key_obj = condition.get_view()->get_object(key_value->get_name()); + + // Get engine lookup key value + auto key = static_cast(COMMAND_LOOKUP.get(key_obj.get_name())); + + // Get node ID + auto next_node_value = next_node.second.get_value_ptr(); + auto next_node_id = next_node_value->get_name(); + + lookup_map[key] = next_node_id; + } + + return lookup_map; + } + default: + throw Error(MSG(err) << "Unknown switch condition type."); + } } bool APIActivityEvent::is_event(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.activity.event.Event"; + nyan::fqon_t api_parent = get_api_parent(obj); + + return api_parent == "engine.util.activity.event.Event"; } activity::event_primer_t APIActivityEvent::get_primer(const nyan::Object &event) { - return ACTIVITY_EVENT_PRIMERS.get(event.get_name()); + return ACTIVITY_EVENT_PRIMER_LOOKUP.get(event.get_name()); } } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/activity.h b/libopenage/gamestate/api/activity.h index 2001aa7548..0b98e62157 100644 --- a/libopenage/gamestate/api/activity.h +++ b/libopenage/gamestate/api/activity.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -9,6 +9,7 @@ #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" @@ -106,7 +107,43 @@ class APIActivityCondition { * * @return Condition function. */ - static activity::condition_t get_condition(const nyan::Object &condition); + static activity::condition_function_t get_condition(const nyan::Object &condition); +}; + +/** + * Helper class for creating Activity switch condition objects from the nyan API. + */ +class APIActivitySwitchCondition { +public: + /** + * Check if a nyan object is a switch condition (type == \p engine.util.activity.switch_condition.SwitchCondition). + * + * @param obj nyan object. + * + * @return true if the object is a switch condition, else false. + */ + static bool is_switch_condition(const nyan::Object &obj); + + /** + * Get the lookup function for a switch condition. + * + * @param condition nyan object. + * + * @return Lookup function. + */ + static activity::switch_function_t get_switch_func(const nyan::Object &condition); + + using lookup_map_t = std::unordered_map; + + /** + * Get the mapping of lookup keys to output node IDs. Lookup keys are resolved from nyan API + * mappings to the engine's lookup key type. + * + * @param condition nyan object. + * + * @return Mapping of lookup keys to output node IDs. + */ + static lookup_map_t get_lookup_map(const nyan::Object &condition); }; /** diff --git a/libopenage/gamestate/api/animation.cpp b/libopenage/gamestate/api/animation.cpp index 86cbfe6014..100ad881dd 100644 --- a/libopenage/gamestate/api/animation.cpp +++ b/libopenage/gamestate/api/animation.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "animation.h" @@ -10,8 +10,7 @@ namespace openage::gamestate::api { bool APIAnimation::is_animation(nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.ability.property.Property"; + return obj.extends("engine.util.animation.Animation"); } const std::string APIAnimation::get_animation_path(const nyan::Object &animation) { diff --git a/libopenage/gamestate/api/definitions.h b/libopenage/gamestate/api/definitions.h index f982571f94..a67d663e0a 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-2025 the openage authors. See copying.md for legal info. #pragma once @@ -8,14 +8,20 @@ #include #include "datastructure/constexpr_map.h" +#include "gamestate/activity/condition/ability_usable.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/condition/target_in_range.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/component/internal/commands/types.h" +#include "gamestate/component/types.h" #include "gamestate/system/types.h" @@ -25,17 +31,206 @@ 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::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 nyan API ability fqon values to internal ability types. + */ +static const auto ABILITY_TYPE_LOOKUP = datastructure::create_const_map( + std::pair("engine.ability.type.Activity", + ability_t::ACTIVITY), + std::pair("engine.ability.type.ApplyContinuousEffect", + ability_t::APPLY_CONTINUOUS_EFFECT), + std::pair("engine.ability.type.ApplyDiscreteEffect", + ability_t::APPLY_DISCRETE_EFFECT), + std::pair("engine.ability.type.Idle", + ability_t::IDLE), + std::pair("engine.ability.type.Move", + ability_t::MOVE), + std::pair("engine.ability.type.LineOfSight", + ability_t::LINE_OF_SIGHT), + std::pair("engine.ability.type.Live", + ability_t::LIVE), + std::pair("engine.ability.type.Resistance", + ability_t::RESISTANCE), + std::pair("engine.ability.type.Selectable", + ability_t::SELECTABLE), + std::pair("engine.ability.type.Turn", + ability_t::TURN)); + +/** + * Maps internal ability types to component types. + */ +static const auto COMPONENT_TYPE_LOOKUP = datastructure::create_const_map( + std::pair(ability_t::ACTIVITY, component::component_t::ACTIVITY), + std::pair(ability_t::APPLY_CONTINUOUS_EFFECT, component::component_t::APPLY_EFFECT), + std::pair(ability_t::APPLY_DISCRETE_EFFECT, component::component_t::APPLY_EFFECT), + std::pair(ability_t::IDLE, component::component_t::IDLE), + std::pair(ability_t::MOVE, component::component_t::MOVE), + std::pair(ability_t::LINE_OF_SIGHT, component::component_t::LINE_OF_SIGHT), + std::pair(ability_t::LIVE, component::component_t::LIVE), + std::pair(ability_t::RESISTANCE, component::component_t::RESISTANCE), + std::pair(ability_t::SELECTABLE, component::component_t::SELECTABLE), + std::pair(ability_t::TURN, component::component_t::TURN)); + +/** + * 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.convert.type.AoE2Convert", // TODO: Remove from API + effect_t::DISCRETE_CONVERT_AOE2), + 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, @@ -49,46 +244,87 @@ static const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map("engine.ability.property.type.Diplomatic"))), std::pair(ability_property_t::LOCK, - nyan::ValueHolder(std::make_shared("engine.ability.property.type.Lock")))); + nyan::ValueHolder(std::make_shared("engine.ability.property.type.Lock"))), + std::pair(ability_property_t::RANGED, + nyan::ValueHolder(std::make_shared("engine.ability.property.type.Ranged")))); + +/** + * 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. */ -static const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map( +static const auto ACTIVITY_NODE_LOOKUP = datastructure::create_const_map( std::pair("engine.util.activity.node.type.Start", activity::node_t::START), std::pair("engine.util.activity.node.type.End", activity::node_t::END), std::pair("engine.util.activity.node.type.Ability", activity::node_t::TASK_SYSTEM), + std::pair("engine.util.activity.node.type.Task", + activity::node_t::TASK_SYSTEM), // TODO: Should this have its own type? std::pair("engine.util.activity.node.type.XORGate", activity::node_t::XOR_GATE), std::pair("engine.util.activity.node.type.XOREventGate", - activity::node_t::XOR_EVENT_GATE)); + activity::node_t::XOR_EVENT_GATE), + std::pair("engine.util.activity.node.type.XORSwitchGate", + activity::node_t::XOR_SWITCH_GATE)); /** * Maps API activity task system types to engine system types. * * TODO: Expand this to include all systems. */ -static const auto ACTIVITY_TASK_SYSTEM_DEFS = datastructure::create_const_map( +static const auto ACTIVITY_TASK_SYSTEM_LOOKUP = 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", - system::system_id_t::MOVE_COMMAND)); + system::system_id_t::MOVE_COMMAND), + std::pair("engine.util.activity.task.type.ClearCommandQueue", + system::system_id_t::CLEAR_COMMAND_QUEUE), + std::pair("engine.util.activity.task.type.PopCommandQueue", + system::system_id_t::POP_COMMAND_QUEUE), + std::pair("engine.util.activity.task.type.MoveToTarget", + system::system_id_t::MOVE_TARGET)); /** * Maps API activity condition types to engine condition types. */ -static const auto ACTIVITY_CONDITIONS = datastructure::create_const_map( +static const auto ACTIVITY_CONDITION_LOOKUP = 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.NextCommandIdle", - std::function(gamestate::activity::next_command_idle)), - std::pair("engine.util.activity.condition.type.NextCommandMove", - std::function(gamestate::activity::next_command_move))); + std::pair("engine.util.activity.condition.type.NextCommand", + std::function(gamestate::activity::next_command)), + std::pair("engine.util.activity.condition.type.TargetInRange", + std::function(gamestate::activity::target_in_range)), + std::pair("engine.util.activity.condition.type.AbilityUsable", + std::function(gamestate::activity::component_enabled))); -static const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map( +/** + * Maps API activity event types to event primer functions. + */ +static const auto ACTIVITY_EVENT_PRIMER_LOOKUP = datastructure::create_const_map( std::pair("engine.util.activity.event.type.CommandInQueue", std::function(gamestate::activity::primer_command_in_queue)), std::pair("engine.util.activity.event.type.Wait", @@ -96,6 +332,22 @@ 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))); + +/** + * Maps API activity switch condition types to nyan API values. + */ +static const auto ACTIVITY_SWITCH_CONDITION_TYPE_LOOKUP = 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. */ @@ -103,4 +355,15 @@ static const auto PATCH_PROPERTY_DEFS = datastructure::create_const_map("engine.patch.property.type.Diplomatic")))); +/** + * Maps API command types to engine command types. + */ +static const auto COMMAND_LOOKUP = datastructure::create_const_map( + std::pair("engine.util.command.type.Idle", + component::command::command_t::IDLE), + std::pair("engine.util.command.type.Move", + component::command::command_t::MOVE), + std::pair("engine.util.command.type.ApplyEffect", + component::command::command_t::APPLY_EFFECT)); + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/effect.cpp b/libopenage/gamestate/api/effect.cpp new file mode 100644 index 0000000000..1cd62afbbb --- /dev/null +++ b/libopenage/gamestate/api/effect.cpp @@ -0,0 +1,50 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#include "effect.h" + +#include "gamestate/api/definitions.h" +#include "gamestate/api/util.h" + + +namespace openage::gamestate::api { + +bool APIEffect::is_effect(const nyan::Object &obj) { + return obj.extends("engine.effect.Effect"); +} + +bool APIEffect::check_type(const nyan::Object &effect, + const effect_t &type) { + nyan::fqon_t api_parent = get_api_parent(effect); + nyan::ValueHolder effect_type = EFFECT_DEFS.get(type); + + auto effect_val = effect_type.get_value_ptr(); + + return effect_val->get_name() == api_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 api_parent = get_api_parent(effect); + + return EFFECT_TYPE_LOOKUP.get(api_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(); + auto property_val = properties->get().at(property_type).get_value_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/object.cpp b/libopenage/gamestate/api/object.cpp new file mode 100644 index 0000000000..be2c0eed1e --- /dev/null +++ b/libopenage/gamestate/api/object.cpp @@ -0,0 +1,14 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "object.h" + +#include "error/error.h" + + +namespace openage::gamestate::api { + +bool APIObject::is_object(const nyan::Object &obj) { + return obj.extends("engine.root.Object"); +} + +} // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/object.h b/libopenage/gamestate/api/object.h new file mode 100644 index 0000000000..05d830793d --- /dev/null +++ b/libopenage/gamestate/api/object.h @@ -0,0 +1,30 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace nyan { +class Object; +} // namespace nyan + + +namespace openage::gamestate::api { + +/** + * Helper class for getting info on generic objects in the nyan API. + */ +class APIObject { +public: + /** + * Check if a nyan object is an API Object (type == \p engine.root.Object). + * + * @param obj nyan object. + * + * @return true if the object is an object, else false. + */ + static bool is_object(const nyan::Object &obj); +}; + +} // namespace diff --git a/libopenage/gamestate/api/patch.cpp b/libopenage/gamestate/api/patch.cpp index 05c2f67d5f..9d6baa787e 100644 --- a/libopenage/gamestate/api/patch.cpp +++ b/libopenage/gamestate/api/patch.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "patch.h" @@ -16,8 +16,7 @@ namespace openage::gamestate::api { bool APIPatch::is_patch(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.patch.Patch"; + return obj.extends("engine.util.patch.Patch"); } bool APIPatch::check_property(const nyan::Object &patch, @@ -38,8 +37,7 @@ const nyan::Object APIPatch::get_property(const nyan::Object &patch, nyan::ValueHolder property_type = PATCH_PROPERTY_DEFS.get(property); std::shared_ptr db_view = patch.get_view(); - std::shared_ptr property_val = std::dynamic_pointer_cast( - properties->get().at(property_type).get_ptr()); + auto property_val = properties->get().at(property_type).get_value_ptr(); return db_view->get_object(property_val->get_name()); } diff --git a/libopenage/gamestate/api/player_setup.cpp b/libopenage/gamestate/api/player_setup.cpp index 4d01cf327d..adef15c1cc 100644 --- a/libopenage/gamestate/api/player_setup.cpp +++ b/libopenage/gamestate/api/player_setup.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "player_setup.h" @@ -13,8 +13,7 @@ namespace openage::gamestate::api { bool APIPlayerSetup::is_player_setup(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.setup.PlayerSetup"; + return obj.extends("engine.util.setup.PlayerSetup"); } const std::vector APIPlayerSetup::get_modifiers(const nyan::Object &player_setup) { @@ -23,7 +22,7 @@ const std::vector APIPlayerSetup::get_modifiers(const nyan::Object auto db_view = player_setup.get_view(); auto modifiers = player_setup.get_set("PlayerSetup.modifiers"); for (auto &modifier_val : modifiers) { - auto modifier_obj_val = std::dynamic_pointer_cast(modifier_val.get_ptr()); + auto modifier_obj_val = modifier_val.get_value_ptr(); auto modifier_obj = db_view->get_object(modifier_obj_val->get_name()); result.push_back(modifier_obj); } @@ -37,7 +36,7 @@ const std::vector APIPlayerSetup::get_start_resources(const nyan:: auto db_view = player_setup.get_view(); auto start_resources = player_setup.get_set("PlayerSetup.starting_resources"); for (auto &resource_val : start_resources) { - auto resource_obj_val = std::dynamic_pointer_cast(resource_val.get_ptr()); + auto resource_obj_val = resource_val.get_value_ptr(); auto resource_obj = db_view->get_object(resource_obj_val->get_name()); result.push_back(resource_obj); } @@ -51,7 +50,7 @@ const std::vector APIPlayerSetup::get_patches(const nyan::Object & auto db_view = player_setup.get_view(); auto patches = player_setup.get_set("PlayerSetup.game_setup"); for (auto &patch_val : patches) { - auto patch_obj_val = std::dynamic_pointer_cast(patch_val.get_ptr()); + auto patch_obj_val = patch_val.get_value_ptr(); auto patch_obj = db_view->get_object(patch_obj_val->get_name()); result.push_back(patch_obj); } diff --git a/libopenage/gamestate/api/property.cpp b/libopenage/gamestate/api/property.cpp index fb4458dd5b..6519514694 100644 --- a/libopenage/gamestate/api/property.cpp +++ b/libopenage/gamestate/api/property.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "property.h" @@ -13,8 +13,7 @@ namespace openage::gamestate::api { bool APIAbilityProperty::is_property(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.ability.property.Property"; + return obj.extends("engine.ability.property.Property"); } const std::vector APIAbilityProperty::get_animations(const nyan::Object &property) { @@ -23,7 +22,7 @@ const std::vector APIAbilityProperty::get_animations(const nyan::O auto db_view = property.get_view(); auto animations = property.get_set("Animated.animations"); for (auto &anim_val : animations) { - auto anim_obj_val = std::dynamic_pointer_cast(anim_val.get_ptr()); + auto anim_obj_val = anim_val.get_value_ptr(); auto anim_obj = db_view->get_object(anim_obj_val->get_name()); result.push_back(anim_obj); } @@ -37,7 +36,7 @@ const std::vector APIAbilityProperty::get_command_sounds(const nya auto db_view = property.get_view(); auto command_sounds = property.get_set("CommandSound.sounds"); for (auto &sound_val : command_sounds) { - auto sound_obj_val = std::dynamic_pointer_cast(sound_val.get_ptr()); + auto sound_obj_val = sound_val.get_value_ptr(); auto sound_obj = db_view->get_object(sound_obj_val->get_name()); result.push_back(sound_obj); } @@ -51,7 +50,7 @@ const std::vector APIAbilityProperty::get_execution_sounds(const n auto db_view = property.get_view(); auto execution_sounds = property.get_set("ExecutionSound.sounds"); for (auto &sound_val : execution_sounds) { - auto sound_obj_val = std::dynamic_pointer_cast(sound_val.get_ptr()); + auto sound_obj_val = sound_val.get_value_ptr(); auto sound_obj = db_view->get_object(sound_obj_val->get_name()); result.push_back(sound_obj); } @@ -65,7 +64,7 @@ const std::vector APIAbilityProperty::get_diplo_stances(const nyan auto db_view = property.get_view(); auto diplo_stances = property.get_set("Diplomatic.stances"); for (auto &diplo_stance_val : diplo_stances) { - auto diplo_stance_obj_val = std::dynamic_pointer_cast(diplo_stance_val.get_ptr()); + auto diplo_stance_obj_val = diplo_stance_val.get_value_ptr(); auto diplo_stance_obj = db_view->get_object(diplo_stance_obj_val->get_name()); result.push_back(diplo_stance_obj); } diff --git a/libopenage/gamestate/api/resistance.cpp b/libopenage/gamestate/api/resistance.cpp new file mode 100644 index 0000000000..3e97b89f24 --- /dev/null +++ b/libopenage/gamestate/api/resistance.cpp @@ -0,0 +1,50 @@ +// Copyright 2024-2025 the openage authors. See copying.md for legal info. + +#include "resistance.h" + +#include "gamestate/api/definitions.h" +#include "gamestate/api/util.h" + + +namespace openage::gamestate::api { + +bool APIResistance::is_resistance(const nyan::Object &obj) { + return obj.extends("engine.resistance.Resistance"); +} + +bool APIResistance::check_effect_type(const nyan::Object &resistance, + const effect_t &type) { + nyan::fqon_t api_parent = get_api_parent(resistance); + nyan::ValueHolder effect_type = RESISTANCE_DEFS.get(type); + + auto effect_val = effect_type.get_value_ptr(); + + return effect_val->get_name() == api_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 api_parent = get_api_parent(resistance); + + return RESISTANCE_TYPE_LOOKUP.get(api_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(); + auto property_val = properties->get().at(property_type).get_value_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/sound.cpp b/libopenage/gamestate/api/sound.cpp index 0af5fafcf4..609d1a2070 100644 --- a/libopenage/gamestate/api/sound.cpp +++ b/libopenage/gamestate/api/sound.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "sound.h" @@ -12,8 +12,7 @@ namespace openage::gamestate::api { bool APISound::is_sound(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.sound.Sound"; + return obj.extends("engine.util.sound.Sound"); } const std::string APISound::get_sound_path(const nyan::Object &sound) { diff --git a/libopenage/gamestate/api/terrain.cpp b/libopenage/gamestate/api/terrain.cpp index ad5e300b47..1c8e080132 100644 --- a/libopenage/gamestate/api/terrain.cpp +++ b/libopenage/gamestate/api/terrain.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "terrain.h" @@ -10,8 +10,7 @@ namespace openage::gamestate::api { bool APITerrain::is_terrain(const nyan::Object &obj) { - nyan::fqon_t immediate_parent = obj.get_parents()[0]; - return immediate_parent == "engine.util.terrain.Terrain"; + return obj.extends("engine.util.terrain.Terrain"); } const std::string APITerrain::get_terrain_path(const nyan::Object &terrain) { @@ -26,8 +25,8 @@ const std::unordered_map APITerrain::get_path_costs(const nya nyan::dict_t path_costs = terrain.get_dict("Terrain.path_costs"); for (const auto &pair : path_costs) { - auto key = std::dynamic_pointer_cast(pair.first.get_ptr()); - auto value = std::dynamic_pointer_cast(pair.second.get_ptr()); + auto key = pair.first.get_value_ptr(); + auto value = pair.second.get_value_ptr(); result.emplace(key->get_name(), value->get()); } diff --git a/libopenage/gamestate/api/types.h b/libopenage/gamestate/api/types.h index 2c44fe1e81..35b036dd69 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-2025 the openage authors. See copying.md for legal info. #pragma once @@ -9,12 +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, + RESISTANCE, + SELECTABLE, TURN, - // TODO + // TODO: other ability types + + // TODO: remove once other ability types are implemented + UNKNOWN, +}; + +/** + * 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_CONVERT_AOE2, // TODO: Remove from API + DISCRETE_FLAC_DECREASE, + DISCRETE_FLAC_INCREASE, + DISCRETE_MAKE_HARVESTABLE, + DISCRETE_SEND_TO_CONTAINER, }; /** @@ -27,6 +56,25 @@ enum class ability_property_t { EXECUTION_SOUND, DIPLOMATIC, LOCK, + RANGED, +}; + +/** + * 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, }; /** @@ -36,4 +84,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/api/util.cpp b/libopenage/gamestate/api/util.cpp index 7e59ebf9bd..293d3c2efc 100644 --- a/libopenage/gamestate/api/util.cpp +++ b/libopenage/gamestate/api/util.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "util.h" @@ -42,4 +42,18 @@ const std::string resolve_file_path(const nyan::Object &obj, const std::string & } } +const nyan::fqon_t &get_api_parent(const nyan::Object &obj) { + if (obj.get_name().starts_with("engine")) { + return obj.get_name(); + } + + for (const auto &parent : obj.get_parents()) { + if (parent.starts_with("engine.")) { + return parent; + } + } + + throw Error(MSG(err) << "No API parent found for object: " << obj.get_name()); +} + } // namespace openage::gamestate::api diff --git a/libopenage/gamestate/api/util.h b/libopenage/gamestate/api/util.h index cad829a205..f3ec3a9446 100644 --- a/libopenage/gamestate/api/util.h +++ b/libopenage/gamestate/api/util.h @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -24,4 +24,18 @@ namespace openage::gamestate::api { */ const std::string resolve_file_path(const nyan::Object &obj, const std::string &path); +/** + * Get the fqon of the first parent of the object that is defined in the + * API namespace (i.e. it's part of the \p engine namespace). + * + * If the object itself is part of the API namespace, it will return the fqon + * of the object. + * + * @param obj nyan object. + * + * @return fqon of the first parent in the API namespace. + * @throws Error if the object has no parents in the API namespace. + */ +const nyan::fqon_t &get_api_parent(const nyan::Object &obj); + } // 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..c9a709d3ea --- /dev/null +++ b/libopenage/gamestate/component/api/apply_effect.h @@ -0,0 +1,108 @@ +// Copyright 2024-2025 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 { + +/** + * Stores runtime information for an ApplyEffect ability of a game entity. + */ +class ApplyEffect final : public APIComponent { +public: + /** + * Creates an ApplyEffect component. + * + * @param loop Event loop that all events from the component are registered on. + * @param ability nyan ability object for the component. + * @param creation_time Ingame creation time of the component. + * @param enabled If true, enable the component at creation time. + */ + ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + const time::time_t &creation_time, + bool enabled = true); + + /** + * Creates an ApplyEffect component. + * + * @param loop Event loop that all events from the component are registered on. + * @param ability nyan ability object for the component. + * @param enabled If true, enable the component at creation time. + */ + ApplyEffect(const std::shared_ptr &loop, + nyan::Object &ability, + bool enabled = true); + + component_t get_type() const override; + + /** + * Get the last time an effect application was initiated that is before the given \p time. + * + * This should be used to determine if the application of the effect is still + * active and when the next application can be initiated. + * + * @param time 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. + * + * This should be used to determine if the effects are under a cooldown, i.e. + * to check if the effects can be applied again. + * + * @param time Simulation time. + * + * @return Curve with the last application times. + */ + const curve::Discrete &get_last_used() const; + + /** + * Record the simulation time when the entity initiates an effect application, + * i.e. it starts using the ability. + * + * @param time Time at which the entity starts using the ability. + */ + void set_init_time(const time::time_t &time); + + /** + * Record the simulation time when the entity applies the effects + * of the ability, i.e. init time + application delay. + * + * @param time Time at which the entity applies 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/idle.h b/libopenage/gamestate/component/api/idle.h index e8b114ff67..ce10a7128b 100644 --- a/libopenage/gamestate/component/api/idle.h +++ b/libopenage/gamestate/component/api/idle.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -7,6 +7,10 @@ namespace openage::gamestate::component { +/** + * Represents an idle state of a game entity, i.e. when it is not + * performing any action or command. + */ class Idle final : public APIComponent { public: using APIComponent::APIComponent; 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..373ed039e5 --- /dev/null +++ b/libopenage/gamestate/component/api/line_of_sight.h @@ -0,0 +1,21 @@ +// Copyright 2024-2025 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 { + +/** + * Stores the line of sight information of a game entity. + */ +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 f7a1f17d98..e9194343ec 100644 --- a/libopenage/gamestate/component/api/live.cpp +++ b/libopenage/gamestate/component/api/live.cpp @@ -2,11 +2,9 @@ #include "live.h" -#include - #include "curve/container/iterator.h" #include "curve/container/map_filter_iterator.h" -#include "curve/discrete.h" +#include "curve/segmented.h" #include "gamestate/component/types.h" @@ -18,20 +16,34 @@ 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); +} + +const attribute_value_t Live::get_attribute(const time::time_t &time, + const nyan::fqon_t &attribute) const { + auto attribute_iterator = this->attributes.at(time, attribute); + if (attribute_iterator) { + auto attribute_curve = **attribute_iterator; + return attribute_curve->get(time); + } + else { + throw Error(MSG(err) << "Attribute not found: " << attribute); + } } 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 4916713cdc..6be45a8bfc 100644 --- a/libopenage/gamestate/component/api/live.h +++ b/libopenage/gamestate/component/api/live.h @@ -13,7 +13,20 @@ #include "time/time.h" -namespace openage::gamestate::component { +namespace openage { + +namespace curve { +template +class Segmented; +} // namespace curve + +namespace gamestate::component { + +/** + * Stores runtime information for a Live ability of a game entity. + * + * Represents the ability of a game entity to have attributes, e.g. health, faith, etc. + */ class Live final : public APIComponent { public: using APIComponent::APIComponent; @@ -29,7 +42,18 @@ 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); + + /** + * Get the value of an attribute at a given time. + * + * @param time The time at which the attribute is queried. + * @param attribute Attribute identifier (fqon of the nyan object). + * + * @return Value of the attribute at the given time. + */ + const attribute_value_t get_attribute(const time::time_t &time, + const nyan::fqon_t &attribute) const; /** * Set the value of an attribute at a given time. @@ -40,16 +64,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/move.h b/libopenage/gamestate/component/api/move.h index 99d87b8d72..01e773cc72 100644 --- a/libopenage/gamestate/component/api/move.h +++ b/libopenage/gamestate/component/api/move.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -8,6 +8,9 @@ namespace openage::gamestate::component { +/** + * Stores the movement information of a game entity. + */ class Move final : public APIComponent { public: using APIComponent::APIComponent; 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..c73468534d --- /dev/null +++ b/libopenage/gamestate/component/api/resistance.h @@ -0,0 +1,24 @@ +// Copyright 2024-2025 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 { + +/** + * Stores information about the resistances of a game entity. + * + * Used together with the ApplyEffect component to allow interactions + * between game entities via effects. + */ +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/api/selectable.h b/libopenage/gamestate/component/api/selectable.h index b37f674444..249172f277 100644 --- a/libopenage/gamestate/component/api/selectable.h +++ b/libopenage/gamestate/component/api/selectable.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -10,6 +10,9 @@ namespace openage::gamestate::component { +/** + * Represents the ability of a game entity to be selected. + */ class Selectable final : public APIComponent { public: using APIComponent::APIComponent; diff --git a/libopenage/gamestate/component/api/turn.h b/libopenage/gamestate/component/api/turn.h index 9506bcd6db..47c5b62528 100644 --- a/libopenage/gamestate/component/api/turn.h +++ b/libopenage/gamestate/component/api/turn.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -10,6 +10,9 @@ namespace openage::gamestate::component { +/** + * Represents the ability of a game entity to change directions. + */ class Turn final : public APIComponent { public: using APIComponent::APIComponent; diff --git a/libopenage/gamestate/component/internal/activity.h b/libopenage/gamestate/component/internal/activity.h index ae0fd73389..d6fed0e176 100644 --- a/libopenage/gamestate/component/internal/activity.h +++ b/libopenage/gamestate/component/internal/activity.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -27,23 +27,26 @@ class Node; namespace component { +/** + * Store the activity flow graph of a game entity. + */ class Activity final : public InternalComponent { public: /** * Creates a new activity component. * * @param loop Event loop that all events from the component are registered on. - * @param start_activity Initial activity flow graph. + * @param activity Activity flow graph. */ Activity(const std::shared_ptr &loop, - const std::shared_ptr &start_activity); + const std::shared_ptr &activity); component_t get_type() const override; /** - * Get the initial activity. + * Get the activity graph of the component. * - * @return Initial activity. + * @return Activity graph. */ const std::shared_ptr &get_start_activity() const; @@ -51,35 +54,39 @@ class Activity final : public InternalComponent { * Get the node in the activity flow graph at a given time. * * @param time Time at which the node is requested. - * @return Current node in the flow graph. + * + * @return Node in the flow graph. */ const std::shared_ptr get_node(const time::time_t &time) const; /** - * Sets the current node in the activity flow graph at a given time. + * Sets the node in the activity flow graph at a given time. * * @param time Time at which the node is set. - * @param node Current node in the flow graph. + * @param node Node in the flow graph. */ void set_node(const time::time_t &time, const std::shared_ptr &node); /** - * Set the current node to the start node of the start activity. + * Initialize the activity flow graph for the component at a given time. * - * @param time Time at which the node is set. + * This sets the current node at \p time to the start node of the activity graph. + * + * @param time Time at which the component is initialized. */ void init(const time::time_t &time); /** - * Add a scheduled event that is waited for to progress in the node graph. + * Store a scheduled event that the activity system waits for to + * progress in the node graph. * * @param event Event to add. */ void add_event(const std::shared_ptr &event); /** - * Cancel all scheduled events. + * Cancel all stored scheduled events. * * @param time Time at which the events are cancelled. */ @@ -87,7 +94,7 @@ class Activity final : public InternalComponent { private: /** - * Initial activity that encapsulates the entity's control flow graph. + * Activity that encapsulates the entity's control flow graph. * * When a game entity is spawned, the activity system should advance in * this activity flow graph to initialize the entity's action state. @@ -97,12 +104,12 @@ class Activity final : public InternalComponent { std::shared_ptr start_activity; /** - * Current node in the activity flow graph. + * Current active node in the activity flow graph over time. */ curve::Discrete> node; /** - * Scheduled events that are waited for to progress in the node graph. + * Scheduled events that the actvity system waits for. */ std::vector> scheduled_events; }; diff --git a/libopenage/gamestate/component/internal/command_queue.cpp b/libopenage/gamestate/component/internal/command_queue.cpp index 4c6963a75d..8ecce5f251 100644 --- a/libopenage/gamestate/component/internal/command_queue.cpp +++ b/libopenage/gamestate/component/internal/command_queue.cpp @@ -1,9 +1,11 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #include "command_queue.h" #include +#include "gamestate/component/internal/commands/apply_effect.h" +#include "gamestate/component/internal/commands/move.h" #include "gamestate/component/types.h" @@ -22,13 +24,52 @@ void CommandQueue::add_command(const time::time_t &time, this->command_queue.insert(time, command); } -curve::Queue> &CommandQueue::get_queue() { +void CommandQueue::set_command(const time::time_t &time, + const std::shared_ptr &command) { + this->command_queue.clear(time); + this->command_queue.insert(time, command); +} + +const curve::Queue> &CommandQueue::get_commands() { return this->command_queue; } -const std::shared_ptr CommandQueue::pop_command(const time::time_t &time) { +void CommandQueue::clear(const time::time_t &time) { + this->command_queue.clear(time); +} + +const std::shared_ptr CommandQueue::pop(const time::time_t &time) { + if (this->command_queue.empty(time)) { + return nullptr; + } + return this->command_queue.pop_front(time); } +const std::shared_ptr CommandQueue::front(const time::time_t &time) const { + if (this->command_queue.empty(time)) { + return nullptr; + } + + return this->command_queue.front(time); +} + +CommandQueue::optional_target_t CommandQueue::get_target(const time::time_t &time) const { + if (this->command_queue.empty(time)) { + return std::monostate{}; + } + + auto command = this->command_queue.front(time); + + // Extract the target from the command + switch (command->get_type()) { + case command::command_t::MOVE: + return std::dynamic_pointer_cast(command)->get_target(); + case command::command_t::APPLY_EFFECT: + return std::dynamic_pointer_cast(command)->get_target(); + default: + return std::monostate{}; + } +} } // namespace openage::gamestate::component diff --git a/libopenage/gamestate/component/internal/command_queue.h b/libopenage/gamestate/component/internal/command_queue.h index fb3179b470..9c7b5c5e15 100644 --- a/libopenage/gamestate/component/internal/command_queue.h +++ b/libopenage/gamestate/component/internal/command_queue.h @@ -3,11 +3,15 @@ #pragma once #include +#include +#include "coord/phys.h" #include "curve/container/queue.h" +#include "curve/discrete.h" #include "gamestate/component/internal/commands/base_command.h" #include "gamestate/component/internal_component.h" #include "gamestate/component/types.h" +#include "gamestate/types.h" #include "time/time.h" @@ -19,6 +23,9 @@ class EventLoop; namespace gamestate::component { +/** + * Stores commands for a game entity. + */ class CommandQueue final : public InternalComponent { public: /** @@ -31,33 +38,83 @@ class CommandQueue final : public InternalComponent { component_t get_type() const override; /** - * Adds a command to the queue. + * Append a command to the queue. * - * @param time Time at which the command is added. + * @param time Time at which the command is appended. * @param command New command. */ void add_command(const time::time_t &time, const std::shared_ptr &command); + /** + * Clear the queue and set the front command. + * + * @param time Time at which the command is set. + * @param command New command. + */ + void set_command(const time::time_t &time, + const std::shared_ptr &command); + /** * Get the command queue. * * @return Command queue. */ - curve::Queue> &get_queue(); + const curve::Queue> &get_commands(); + + /** + * Clear all commands in the queue. + * + * @param time Time at which the queue is cleared. + */ + void clear(const time::time_t &time); /** - * Get the command in the front of the queue. + * Get the command in the front of the queue and remove it. + * + * Unlike curve::Queue::front(), calling this on an empty queue is + * not undefined behavior. + * + * @param time Time at which the command is popped. + * + * @return Command in the front of the queue or nullptr if the queue is empty. + */ + const std::shared_ptr pop(const time::time_t &time); + + /** + * get the command at the front of the queue. * * @param time Time at which the command is retrieved. * * @return Command in the front of the queue or nullptr if the queue is empty. */ - const std::shared_ptr pop_command(const time::time_t &time); + const std::shared_ptr front(const time::time_t &time) const; + + /** + * Target type with several possible representations. + * + * Can be: + * - std::monostate: No target. + * - coord::phys3: Position in the game world. + * - entity_id_t: ID of another entity. + */ + using optional_target_t = std::variant; + + /** + * Get the target of the entity at the given time. + * + * The target may be empty if the command queue is empty or if the command + * has no target. + * + * @return Target of the entity. + */ + optional_target_t get_target(const time::time_t &time) const; private: /** * Command queue. + * + * Stores the commands received by the entity over time. */ curve::Queue> command_queue; }; 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/internal/ownership.h b/libopenage/gamestate/component/internal/ownership.h index ab4b30bed3..2d2f52ed68 100644 --- a/libopenage/gamestate/component/internal/ownership.h +++ b/libopenage/gamestate/component/internal/ownership.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -19,6 +19,9 @@ class EventLoop; namespace gamestate::component { +/** + * Stores ownership information of a game entity. + */ class Ownership final : public InternalComponent { public: /** @@ -58,7 +61,7 @@ class Ownership final : public InternalComponent { private: /** - * Owner ID storage over time. + * ID of the entity owner over time. */ curve::Discrete owner; }; diff --git a/libopenage/gamestate/component/internal/position.h b/libopenage/gamestate/component/internal/position.h index ff4cab0da2..bb79c21013 100644 --- a/libopenage/gamestate/component/internal/position.h +++ b/libopenage/gamestate/component/internal/position.h @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -20,6 +20,11 @@ class EventLoop; namespace gamestate::component { +/** + * Stores positional information about a game entity, i.e. location and + * direction. + * + */ class Position final : public InternalComponent { public: /** 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/definitions.h b/libopenage/gamestate/definitions.h index a2d3f113d4..8e514571cb 100644 --- a/libopenage/gamestate/definitions.h +++ b/libopenage/gamestate/definitions.h @@ -1,8 +1,10 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once #include "coord/phys.h" +#include "gamestate/types.h" + /** * Hardcoded definitions for parameters used in the gamestate. @@ -16,4 +18,18 @@ namespace openage::gamestate { */ constexpr coord::phys3 WORLD_ORIGIN = coord::phys3{0, 0, 0}; +/** + * Starting point for entity IDs. + * + * IDs 0-99 are reserved. + */ +constexpr entity_id_t GAME_ENTITY_ID_START = 100; + +/** + * Starting point for player IDs. + * + * ID 0 is reserved. + */ +constexpr player_id_t PLAYER_ID_START = 1; + } // namespace openage::gamestate diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index 6e1a4c825f..e0e7e8abee 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -22,10 +22,16 @@ #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/ability.h" #include "gamestate/api/activity.h" +#include "gamestate/api/util.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" @@ -74,17 +80,21 @@ std::shared_ptr create_test_activity() { idle->set_system_id(system::system_id_t::IDLE); // branch 1: check if the entity is moveable - activity::condition_t command_branch = [&](const time::time_t & /* time */, - const std::shared_ptr &entity) { + activity::condition_function_t command_branch = [&](const time::time_t & /* time */, + const std::shared_ptr &entity, + const std::shared_ptr & /* state */, + const std::shared_ptr & /* api_object */) { return entity->has_component(component::component_t::MOVE); }; - condition_moveable->add_output(condition_command, command_branch); + condition_moveable->add_output(condition_command, + {nullptr, // never used by condition func + command_branch}); // default: if it's not moveable, go straight to the end condition_moveable->set_default(end); // branch 1: check if there is already a command in the queue - condition_command->add_output(move, gamestate::activity::command_in_queue); + condition_command->add_output(move, {nullptr, gamestate::activity::command_in_queue}); // default: if there is no command, wait for a command condition_command->set_default(wait_for_command); @@ -106,8 +116,6 @@ std::shared_ptr create_test_activity() { } EntityFactory::EntityFactory() : - next_entity_id{0}, - next_player_id{0}, render_factory{nullptr} { } @@ -121,7 +129,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()); @@ -165,48 +173,78 @@ void EntityFactory::init_components(const std::shared_ptr activity_ability; for (const auto &ability_val : abilities) { - auto ability_fqon = std::dynamic_pointer_cast(ability_val.get_ptr())->get_name(); + auto ability_fqon = ability_val.get_value_ptr()->get_name(); auto ability_obj = owner_db_view->get_object(ability_fqon); - auto ability_parent = ability_obj.get_parents()[0]; - if (ability_parent == "engine.ability.type.Move") { + auto ability_parent = api::get_api_parent(ability_obj); + auto ability_type = api::APIAbility::get_type(ability_obj); + switch (ability_type) { + case api::ability_t::MOVE: { auto move = std::make_shared(loop, ability_obj); entity->add_component(move); + break; } - else if (ability_parent == "engine.ability.type.Turn") { + case api::ability_t::TURN: { auto turn = std::make_shared(loop, ability_obj); entity->add_component(turn); + break; } - else if (ability_parent == "engine.ability.type.Idle") { + case api::ability_t::IDLE: { auto idle = std::make_shared(loop, ability_obj); entity->add_component(idle); + break; } - else if (ability_parent == "engine.ability.type.Live") { + case api::ability_t::LIVE: { auto live = std::make_shared(loop, ability_obj); entity->add_component(live); auto attr_settings = ability_obj.get_set("Live.attributes"); for (auto &setting : attr_settings) { - auto setting_obj_val = std::dynamic_pointer_cast(setting.get_ptr()); + auto setting_obj_val = setting.get_value_ptr(); auto setting_obj = owner_db_view->get_object(setting_obj_val->get_name()); auto attribute = setting_obj.get_object("AttributeSetting.attribute"); auto start_value = setting_obj.get_int("AttributeSetting.starting_value"); live->add_attribute(time::TIME_MIN, attribute.get_name(), - std::make_shared>(loop, - 0, - "", - nullptr, - start_value)); + std::make_shared>( + loop, + 0, + "", + nullptr, + start_value)); } + break; } - else if (ability_parent == "engine.ability.type.Activity") { + case api::ability_t::ACTIVITY: { activity_ability = ability_obj; + break; } - else if (ability_parent == "engine.ability.type.Selectable") { + case api::ability_t::SELECTABLE: { auto selectable = std::make_shared(loop, ability_obj); entity->add_component(selectable); + break; + } + case api::ability_t::APPLY_DISCRETE_EFFECT: + [[fallthrough]]; + case api::ability_t::APPLY_CONTINUOUS_EFFECT: { + auto apply_effect = std::make_shared(loop, ability_obj); + entity->add_component(apply_effect); + break; + } + case api::ability_t::RESISTANCE: { + auto resistance = std::make_shared(loop, ability_obj); + entity->add_component(resistance); + break; + } + case api::ability_t::LINE_OF_SIGHT: { + auto line_of_sight = std::make_shared(loop, ability_obj); + entity->add_component(line_of_sight); + break; + } + default: + // TODO: Change verbosity from SPAM to INFO once we cover all ability types + log::log(SPAM << "Entity has unrecognized ability type: " << ability_parent); } } @@ -273,6 +311,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()}; } @@ -303,7 +344,20 @@ void EntityFactory::init_activity(const std::shared_ptr(activity_node); - auto output_fqon = nyan_node.get("Ability.next")->get_name(); + + auto api_parent = api::get_api_parent(nyan_node); + nyan::memberid_t member_name; + if (api_parent == "engine.util.activity.node.type.Ability") { + member_name = "Ability.next"; + } + else if (api_parent == "engine.util.activity.node.type.Task") { + member_name = "Task.next"; + } + else { + throw Error{ERR << "Node type '" << api_parent << "' cannot be used to get the next node."}; + } + + auto output_fqon = nyan_node.get(member_name)->get_name(); auto output_id = visited[output_fqon]; auto output_node = node_id_map[output_id]; task_system->add_output(output_node); @@ -313,14 +367,16 @@ void EntityFactory::init_activity(const std::shared_ptr(activity_node); auto conditions = nyan_node.get("XORGate.next"); for (auto &condition : conditions->get()) { - auto condition_value = std::dynamic_pointer_cast(condition.get_ptr()); - auto condition_obj = owner_db_view->get_object(condition_value->get_name()); + auto condition_value = condition.get_value_ptr(); + auto condition_obj = owner_db_view->get_object_ptr(condition_value->get_name()); - auto output_value = condition_obj.get("Condition.next")->get_name(); + auto output_value = condition_obj->get("Condition.next")->get_name(); auto output_id = visited[output_value]; auto output_node = node_id_map[output_id]; - xor_gate->add_output(output_node, api::APIActivityCondition::get_condition(condition_obj)); + // TODO: Replace nullptr with object + xor_gate->add_output(output_node, + {condition_obj, api::APIActivityCondition::get_condition(*condition_obj)}); } auto default_fqon = nyan_node.get("XORGate.default")->get_name(); @@ -333,10 +389,10 @@ void EntityFactory::init_activity(const std::shared_ptr(activity_node); auto next = nyan_node.get("XOREventGate.next"); for (auto &next_node : next->get()) { - auto event_value = std::dynamic_pointer_cast(next_node.first.get_ptr()); + auto event_value = next_node.first.get_value_ptr(); auto event_obj = owner_db_view->get_object(event_value->get_name()); - auto next_node_value = std::dynamic_pointer_cast(next_node.second.get_ptr()); + auto next_node_value = next_node.second.get_value_ptr(); auto next_node_obj = owner_db_view->get_object(next_node_value->get_name()); auto output_id = visited[next_node_obj.get_name()]; @@ -346,6 +402,27 @@ void EntityFactory::init_activity(const std::shared_ptr(activity_node); + auto switch_value = nyan_node.get("XORSwitchGate.switch"); + auto switch_obj = owner_db_view->get_object_ptr(switch_value->get_name()); + + auto switch_condition_func = api::APIActivitySwitchCondition::get_switch_func(*switch_obj); + xor_switch_gate->set_switch_func({switch_obj, switch_condition_func}); + + auto lookup_map = api::APIActivitySwitchCondition::get_lookup_map(*switch_obj); + for (const auto &[key, node_id] : lookup_map) { + auto output_id = visited[node_id]; + auto output_node = node_id_map[output_id]; + xor_switch_gate->set_output(output_node, key); + } + + auto default_fqon = nyan_node.get("XORSwitchGate.default")->get_name(); + auto default_id = visited[default_fqon]; + auto default_node = node_id_map[default_id]; + xor_switch_gate->set_default(default_node); + break; + } default: throw Error{ERR << "Unknown activity node type of node: " << current_node.first}; } diff --git a/libopenage/gamestate/entity_factory.h b/libopenage/gamestate/entity_factory.h index 11efbe3f2e..f3f4e5ab1c 100644 --- a/libopenage/gamestate/entity_factory.h +++ b/libopenage/gamestate/entity_factory.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -7,6 +7,7 @@ #include +#include "gamestate/definitions.h" #include "gamestate/types.h" @@ -119,12 +120,12 @@ class EntityFactory { /** * ID of the next game entity to be created. */ - entity_id_t next_entity_id; + entity_id_t next_entity_id = GAME_ENTITY_ID_START; /** * ID of the next player to be created. */ - player_id_t next_player_id; + player_id_t next_player_id = PLAYER_ID_START; /** * Factory for creating connector objects to the renderer which make game entities displayable. diff --git a/libopenage/gamestate/event/drag_select.cpp b/libopenage/gamestate/event/drag_select.cpp index efb43b0ab1..1100b5088a 100644 --- a/libopenage/gamestate/event/drag_select.cpp +++ b/libopenage/gamestate/event/drag_select.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2023 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "drag_select.h" @@ -32,7 +32,7 @@ void DragSelectHandler::invoke(openage::event::EventLoop & /* loop */, const param_map ¶ms) { auto gstate = std::dynamic_pointer_cast(state); - size_t controlled_id = params.get("controlled", 0); + auto controlled_id = params.get("controlled", 0); Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); Eigen::Matrix4f cam_matrix = params.get("camera_matrix", id_matrix); diff --git a/libopenage/gamestate/event/send_command.cpp b/libopenage/gamestate/event/send_command.cpp index 8530d6340d..1f5cbc2d16 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-2025 the openage authors. See copying.md for legal info. #include "send_command.h" @@ -6,6 +6,7 @@ #include "coord/phys.h" #include "gamestate/component/internal/command_queue.h" +#include "gamestate/component/internal/commands/apply_effect.h" #include "gamestate/component/internal/commands/idle.h" #include "gamestate/component/internal/commands/move.h" #include "gamestate/component/types.h" @@ -19,8 +20,8 @@ namespace component { class CommandQueue; namespace command { -class IdleCommand; -class MoveCommand; +class Idle; +class Move; } // namespace command } // namespace component @@ -64,16 +65,20 @@ void SendCommandHandler::invoke(openage::event::EventLoop & /* loop */, entity->get_component(component::component_t::COMMANDQUEUE)); switch (command_type) { - case component::command::command_t::IDLE: - command_queue->add_command(time, std::make_shared()); + case component::command::command_t::IDLE: { + command_queue->set_command(time, std::make_shared()); break; - case component::command::command_t::MOVE: - command_queue->add_command( - time, - std::make_shared( - params.get("target", - coord::phys3{0, 0, 0}))); + } + case component::command::command_t::MOVE: { + auto target_pos = params.get("target", coord::phys3{0, 0, 0}); + command_queue->set_command(time, std::make_shared(target_pos)); + break; + } + case component::command::command_t::APPLY_EFFECT: { + auto target_id = params.get("target", 0); + command_queue->set_command(time, std::make_shared(target_id)); break; + } default: break; } diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 66f6df9be7..94bd9f8a7e 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "spawn_entity.h" @@ -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" @@ -192,7 +193,7 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, } // Create entity - player_id_t owner_id = params.get("owner", 0); + auto owner_id = params.get("owner", 0); auto entity = this->factory->add_game_entity(this->loop, gstate, owner_id, nyan_entity); // Setup components @@ -209,6 +210,8 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, 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/game_state.cpp b/libopenage/gamestate/game_state.cpp index 4bf0aaa348..695423d53a 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "game_state.h" @@ -63,6 +63,14 @@ const std::shared_ptr &GameState::get_map() const { return this->map; } +bool GameState::has_game_entity(entity_id_t id) const { + return this->game_entities.contains(id); +} + +bool GameState::has_player(player_id_t id) const { + return this->players.contains(id); +} + const std::shared_ptr &GameState::get_mod_manager() const { return this->mod_manager; } diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index 98cdbc187a..02ee31a5d3 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -108,6 +108,24 @@ class GameState : public openage::event::State { */ const std::shared_ptr &get_map() const; + /** + * Check whether a game entity with the given ID exists. + * + * @param id ID of the game entity. + * + * @return true if the game entity exists, false otherwise. + */ + bool has_game_entity(entity_id_t id) const; + + /** + * Check whether a player with the given ID exists. + * + * @param id ID of the player. + * + * @return true if the player exists, false otherwise. + */ + bool has_player(player_id_t id) const; + /** * TODO: Only for testing. */ diff --git a/libopenage/gamestate/system/CMakeLists.txt b/libopenage/gamestate/system/CMakeLists.txt index ec2ea97945..57a3ef3591 100644 --- a/libopenage/gamestate/system/CMakeLists.txt +++ b/libopenage/gamestate/system/CMakeLists.txt @@ -1,6 +1,9 @@ add_sources(libopenage activity.cpp + apply_effect.cpp + command_queue.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..33b7982b3b 100644 --- a/libopenage/gamestate/system/activity.cpp +++ b/libopenage/gamestate/system/activity.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "activity.h" @@ -16,9 +16,12 @@ #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/command_queue.h" #include "gamestate/system/idle.h" #include "gamestate/system/move.h" #include "util/fixed_point.h" @@ -87,8 +90,9 @@ void Activity::advance(const time::time_t &start_time, auto node = std::static_pointer_cast(current_node); auto next_id = node->get_default()->get_id(); for (auto &condition : node->get_conditions()) { - auto condition_func = condition.second; - if (condition_func(start_time, entity)) { + auto condition_obj = condition.second.api_object; + auto condition_func = condition.second.function; + if (condition_func(start_time, entity, state, condition_obj)) { next_id = condition.first; break; } @@ -111,6 +115,18 @@ 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 switch_condition_obj = node->get_switch_func().api_object; + auto switch_condition_func = node->get_switch_func().function; + auto key = switch_condition_func(start_time, entity, state, switch_condition_obj); + 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,15 +141,23 @@ 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_command(entity, state, start_time); + break; case system_id_t::IDLE: return Idle::idle(entity, start_time); break; case system_id_t::MOVE_COMMAND: return Move::move_command(entity, state, start_time); break; - case system_id_t::MOVE_DEFAULT: - // TODO: replace destination value with a parameter - return Move::move_default(entity, state, {1, 1, 1}, start_time); + case system_id_t::MOVE_TARGET: + return Move::move_target(entity, state, start_time); + break; + case system_id_t::CLEAR_COMMAND_QUEUE: + return CommandQueue::clear_queue(entity, start_time); + break; + case system_id_t::POP_COMMAND_QUEUE: + return CommandQueue::pop_command(entity, start_time); break; default: throw Error{ERR << "Unhandled subsystem " << static_cast(system_id)}; diff --git a/libopenage/gamestate/system/activity.h b/libopenage/gamestate/system/activity.h index 11dcce68ca..4dd0237613 100644 --- a/libopenage/gamestate/system/activity.h +++ b/libopenage/gamestate/system/activity.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -27,6 +27,9 @@ class Activity { /** * Advance in the activity flow graph of the game entity. * + * Visits and executes actions for the current node until a node that + * requires an event to be triggered is reached. + * * @param start_time Start time of change. * @param entity Game entity. */ diff --git a/libopenage/gamestate/system/apply_effect.cpp b/libopenage/gamestate/system/apply_effect.cpp new file mode 100644 index 0000000000..7c45f4b8b1 --- /dev/null +++ b/libopenage/gamestate/system/apply_effect.cpp @@ -0,0 +1,237 @@ +// Copyright 2024-2025 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/api/turn.h" +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/component/internal/commands/apply_effect.h" +#include "gamestate/component/internal/position.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_command(const std::shared_ptr &entity, + const std::shared_ptr &state, + const time::time_t &start_time) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto command = std::dynamic_pointer_cast( + command_queue->pop(start_time)); + + if (not command) [[unlikely]] { + log::log(MSG(warn) << "Command is not a apply effect command."); + return time::time_t::from_int(0); + } + + auto resistor_id = command->get_target(); + auto resistor = state->get_game_entity(resistor_id); + + return ApplyEffect::apply_effect(entity, state, resistor, start_time); +} + + +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) { + time::time_t total_time = 0; + + // rotate towards resistor + auto turn_component_effector = std::dynamic_pointer_cast( + effector->get_component(component::component_t::TURN)); + auto turn_ability = turn_component_effector->get_ability(); + auto turn_speed = turn_ability.get("Turn.turn_speed"); + + auto pos_component_effector = std::dynamic_pointer_cast( + effector->get_component(component::component_t::POSITION)); + auto pos_component_resistor = std::dynamic_pointer_cast( + resistor->get_component(component::component_t::POSITION)); + auto effector_pos = pos_component_effector->get_positions().get(start_time); + auto resistor_pos = pos_component_resistor->get_positions().get(start_time); + auto effector_angle = pos_component_effector->get_angles().get(start_time); + + auto path_vector = resistor_pos - effector_pos; + auto path_angle = path_vector.to_angle(); + + if (not turn_speed->is_infinite_positive()) { + auto angle_diff = path_angle.abs_diff(effector_angle); + + if (angle_diff > 180) { + // always use the smaller angle + angle_diff = coord::phys_angle_t::same_type_but_unsigned{360} - angle_diff; + } + + double turn_time = angle_diff.to_double() / turn_speed->get(); + total_time += turn_time; + + // TODO: Delay application of effect until the turn is finished + } + pos_component_effector->set_angle(start_time + total_time, path_angle); + + // start applications + 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) { + auto batch_obj_val = batch.get_value_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) { + auto effect_obj_val = batch_effect.get_value_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) { + auto resistance_obj_val = resistance.get_value_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); + } + + // application time + // 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: + [[fallthrough]]; + 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); + + if (effect_type == api::effect_t::DISCRETE_FLAC_DECREASE) { + // negate the applied value for decrease effects + applied_value = -applied_value; + } + + // 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); + + // Calculate the new attribute value + auto current_value = live_component->get_attribute(start_time, attribute.get_name()); + auto new_value = current_value + applied_value; + + // Apply the attribute change to the live component + live_component->set_attribute(start_time + delay, attribute.get_name(), new_value); + } break; + default: + throw Error(MSG(err) << "Effect type not implemented: " << static_cast(effect_type)); + } + } + + // properties + handle_animated(effector, effect_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..64c6aff15b --- /dev/null +++ b/libopenage/gamestate/system/apply_effect.h @@ -0,0 +1,86 @@ +// Copyright 2024-2025 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 from a command fetched from the command queue. + * + * The front command in the command queue is expected to be of type `ApplyEffect`. If + * not, the command is ignored and a runtime of 0 is returned. + * + * Consumes (pops) the front command from the command queue. + * + * @param entity Game entity applying the effects. + * @param state Game state. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ + static const time::time_t apply_effect_command(const std::shared_ptr &entity, + const std::shared_ptr &state, + const time::time_t &start_time); + +private: + /** + * Apply the effect of an ability of one game entity (\p effector) to another + * game entity (\p resistor). + * + * The effector requires an enabled `ApplyEffect` and `Turn` component. + * The entity requires an enabled `Resistance` component. + * + * The effector takes the following actions: + * - Rotate towards the resistor. + * - Apply the effects of the ability to the resistor. + * + * @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); + + /** + * 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/command_queue.cpp b/libopenage/gamestate/system/command_queue.cpp new file mode 100644 index 0000000000..4879f0b603 --- /dev/null +++ b/libopenage/gamestate/system/command_queue.cpp @@ -0,0 +1,32 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#include "command_queue.h" + +#include "gamestate/game_entity.h" +#include "gamestate/component/internal/command_queue.h" +#include "gamestate/component/types.h" + + +namespace openage::gamestate::system { + +const time::time_t CommandQueue::clear_queue(const std::shared_ptr &entity, + const time::time_t &start_time) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + command_queue->clear(start_time); + + return start_time; +} + +const time::time_t CommandQueue::pop_command(const std::shared_ptr &entity, + const time::time_t &start_time) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + + command_queue->pop(start_time); + + return start_time; +} + +} // namespace diff --git a/libopenage/gamestate/system/command_queue.h b/libopenage/gamestate/system/command_queue.h new file mode 100644 index 0000000000..e8f9ea09b9 --- /dev/null +++ b/libopenage/gamestate/system/command_queue.h @@ -0,0 +1,41 @@ +// Copyright 2025-2025 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "time/time.h" + + +namespace openage::gamestate { +class GameEntity; + +namespace system { + +class CommandQueue { +public: + /** + * Clear the command queue of the entity. + * + * @param entity Game entity. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ + static const time::time_t clear_queue(const std::shared_ptr &entity, + const time::time_t &start_time); + + /** + * Pop the front command from the command queue of the entity. + * + * @param entity Game entity. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ + static const time::time_t pop_command(const std::shared_ptr &entity, + const time::time_t &start_time); +}; + +} // namespace system +} // namespace openage::gamestate 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/idle.h b/libopenage/gamestate/system/idle.h index eb4434fdb7..4f3446e496 100644 --- a/libopenage/gamestate/system/idle.h +++ b/libopenage/gamestate/system/idle.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -17,6 +17,8 @@ class Idle { /** * Let a game entity idle. * + * The entity requires an enabled `Idle` component. + * * This does not change the state of a unit. It only changes its animation and * sounds. * diff --git a/libopenage/gamestate/system/move.cpp b/libopenage/gamestate/system/move.cpp index 5a44baf113..6fb1f5c54e 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" @@ -33,6 +34,12 @@ namespace openage::gamestate::system { +// helper type for the visitor processing the move_target +template +struct overloaded : Ts... { + using Ts::operator()...; +}; + std::vector find_path(const std::shared_ptr &pathfinder, path::grid_id_t grid_id, @@ -80,8 +87,8 @@ const time::time_t Move::move_command(const std::shared_ptr( entity->get_component(component::component_t::COMMANDQUEUE)); - auto command = std::dynamic_pointer_cast( - command_queue->pop_command(start_time)); + auto command = std::dynamic_pointer_cast( + command_queue->pop(start_time)); if (not command) [[unlikely]] { log::log(MSG(warn) << "Command is not a move command."); @@ -91,6 +98,35 @@ const time::time_t Move::move_command(const std::shared_ptrget_target(), start_time); } +const time::time_t Move::move_target(const std::shared_ptr &entity, + const std::shared_ptr &state, + const time::time_t &start_time) { + auto command_queue = std::dynamic_pointer_cast( + entity->get_component(component::component_t::COMMANDQUEUE)); + auto target = command_queue->get_target(start_time); + + return std::visit( + overloaded{ + [&](const gamestate::entity_id_t &target_id) { + auto target_entity = state->get_game_entity(target_id); + + auto position = std::dynamic_pointer_cast( + target_entity->get_component(component::component_t::POSITION)); + + auto target_pos = position->get_positions().get(start_time); + + return Move::move_default(entity, state, target_pos, start_time); + }, + [&](const coord::phys3 &target_pos) { + return Move::move_default(entity, state, target_pos, start_time); + }, + [&](const std::monostate &) { + log::log(WARN << "Entity " << entity->get_id() << " has no target at time " << start_time); + return time::time_t::from_int(0); + }}, + target); +} + const time::time_t Move::move_default(const std::shared_ptr &entity, const std::shared_ptr &state, @@ -138,15 +174,11 @@ const time::time_t Move::move_default(const std::shared_ptris_infinite_positive()) { - auto angle_diff = path_angle - current_angle; - if (angle_diff < 0) { - // get the positive difference - angle_diff = angle_diff * -1; - } + auto angle_diff = path_angle.abs_diff(current_angle); + if (angle_diff > 180) { // always use the smaller angle - angle_diff = angle_diff - 360; - angle_diff = angle_diff * -1; + angle_diff = coord::phys_angle_t::same_type_but_unsigned{360} - angle_diff; } // Set an intermediate position keyframe to halt the game entity @@ -173,15 +205,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/move.h b/libopenage/gamestate/system/move.h index 346c1c0aa6..c3b5bf445e 100644 --- a/libopenage/gamestate/system/move.h +++ b/libopenage/gamestate/system/move.h @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #pragma once @@ -19,6 +19,11 @@ class Move { /** * Move a game entity to a destination from a move command. * + * The front command in the command queue is expected to be of type `Move`. If + * not, the command is ignored and a runtime of 0 is returned. + * + * Consumes (pops) the move command from the command queue. + * * @param entity Game entity. * @param state Game state. * @param start_time Start time of change. @@ -29,9 +34,36 @@ class Move { const std::shared_ptr &state, const time::time_t &start_time); + /** + * Move a game entity to the current target of the game entity. + * + * The target is fetched from the command queue. If no target is set, the + * command is ignored and a runtime of 0 is returned. + * + * @param entity Game entity. + * @param state Game state. + * @param start_time Start time of change. + * + * @return Runtime of the change in simulation time. + */ + static const time::time_t move_target(const std::shared_ptr &entity, + const std::shared_ptr &state, + const time::time_t &start_time); + +private: /** * Move a game entity to a destination. * + * The entity requires an enabled `Move` and `Turn` component. + * + * The entity takes the following actions: + * - use the pathfinding system to find a waypoint path to the destination + * - for each waypoint: + * - rotate towards the waypoint + * - move to the waypoint + * + * These action mirror the movement behavior of units in Age of Empires II. + * * @param entity Game entity. * @param state Game state. * @param destination Destination coordinates. 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..5be3385e8a 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-2025 the openage authors. See copying.md for legal info. #pragma once @@ -11,12 +11,19 @@ namespace openage::gamestate::system { enum class system_id_t { NONE, + // ability systems + APPLY_EFFECT, + IDLE, MOVE_COMMAND, - MOVE_DEFAULT, + MOVE_TARGET, ACTIVITY_ADVANCE, + + // tasks + CLEAR_COMMAND_QUEUE, + POP_COMMAND_QUEUE, }; } // namespace openage::gamestate::system diff --git a/libopenage/input/controller/game/controller.cpp b/libopenage/input/controller/game/controller.cpp index 96ccf53429..7f3429f02e 100644 --- a/libopenage/input/controller/game/controller.cpp +++ b/libopenage/input/controller/game/controller.cpp @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #include "controller.h" @@ -14,6 +14,7 @@ #include "gamestate/game_state.h" #include "gamestate/simulation.h" #include "input/controller/game/binding_context.h" +#include "renderer/texture.h" #include "time/clock.h" #include "time/time_loop.h" @@ -23,13 +24,13 @@ namespace openage::input::game { -Controller::Controller(const std::unordered_set &controlled_factions, - size_t active_faction_id) : +Controller::Controller(const std::unordered_set &controlled_factions, + gamestate::player_id_t active_faction_id) : controlled_factions{controlled_factions}, active_faction_id{active_faction_id}, outqueue{} {} -void Controller::set_control(size_t faction_id) { +void Controller::set_control(gamestate::player_id_t faction_id) { std::unique_lock lock{this->mutex}; if (this->controlled_factions.find(faction_id) != this->controlled_factions.end()) { @@ -37,7 +38,7 @@ void Controller::set_control(size_t faction_id) { } } -size_t Controller::get_controlled() const { +gamestate::player_id_t Controller::get_controlled() const { std::unique_lock lock{this->mutex}; return this->active_faction_id; @@ -92,6 +93,14 @@ bool Controller::process(const event_arguments &ev_args, const std::shared_ptr &Controller::get_id_texture() const { + return this->id_texture; +} + +void Controller::set_id_texture(const std::shared_ptr &id_texture) { + this->id_texture = id_texture; +} + void Controller::set_drag_select_start(const coord::input &start) { std::unique_lock lock{this->mutex}; @@ -146,14 +155,30 @@ void setup_defaults(const std::shared_ptr &ctx, ctx->bind(ev_mouse_lmb_ctrl, create_entity_action); - binding_func_t move_entity{[&](const event_arguments &args, - const std::shared_ptr controller) { - auto mouse_pos = args.mouse.to_phys3(camera); - event::EventHandler::param_map::map_t params{ - {"type", gamestate::component::command::command_t::MOVE}, - {"target", mouse_pos}, - {"entity_ids", controller->get_selected()}, - }; + binding_func_t interact_entity{[&](const event_arguments &args, + const std::shared_ptr controller) { + auto id_texture = controller->get_id_texture(); + auto texture_data = id_texture->into_data(); + + event::EventHandler::param_map::map_t params{}; + + gamestate::entity_id_t target_entity_id = texture_data.read_pixel(args.mouse.x, args.mouse.y); + log::log(DBG << "Targeting entity ID: " << target_entity_id); + if (target_entity_id == 0) { + auto mouse_pos = args.mouse.to_phys3(camera); + params = { + {"type", gamestate::component::command::command_t::MOVE}, + {"target", mouse_pos}, + {"entity_ids", controller->get_selected()}, + }; + } + else { + params = { + {"type", gamestate::component::command::command_t::APPLY_EFFECT}, + {"target", target_entity_id}, + {"entity_ids", controller->get_selected()}, + }; + } auto event = simulation->get_event_loop()->create_event( "game.send_command", @@ -164,7 +189,7 @@ void setup_defaults(const std::shared_ptr &ctx, return event; }}; - binding_action move_entity_action{forward_action_t::SEND, move_entity}; + binding_action move_entity_action{forward_action_t::SEND, interact_entity}; Event ev_mouse_rmb{ event_class::MOUSE_BUTTON, Qt::MouseButton::RightButton, diff --git a/libopenage/input/controller/game/controller.h b/libopenage/input/controller/game/controller.h index a9d690f622..a6740030e2 100644 --- a/libopenage/input/controller/game/controller.h +++ b/libopenage/input/controller/game/controller.h @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the openage authors. See copying.md for legal info. +// Copyright 2021-2025 the openage authors. See copying.md for legal info. #pragma once @@ -15,6 +15,10 @@ namespace openage { +namespace renderer { +class Texture2d; +} // namespace renderer + namespace gamestate { class GameSimulation; } @@ -36,8 +40,14 @@ class BindingContext; */ class Controller : public std::enable_shared_from_this { public: - Controller(const std::unordered_set &controlled_factions, - size_t active_faction_id); + /** + * Create a new game controller. + * + * @param controlled_factions Factions that can be managed by the controller. + * @param active_faction_id Current active faction ID. + */ + Controller(const std::unordered_set &controlled_factions, + gamestate::player_id_t active_faction_id); ~Controller() = default; @@ -47,7 +57,7 @@ class Controller : public std::enable_shared_from_this { * * @param faction_id ID of the new active faction. */ - void set_control(size_t faction_id); + void set_control(gamestate::player_id_t faction_id); /** * Get the ID of the faction actively controlled by the controller. @@ -80,6 +90,26 @@ class Controller : public std::enable_shared_from_this { */ bool process(const event_arguments &ev_args, const std::shared_ptr &ctx); + /** + * Get the texture that maps pixels to entity IDs. + * + * Each pixel value in the texture corresponds to an entity ID. This + * mapping may be used for interacting with entities in the game world. + * + * @return ID texture. + */ + const std::shared_ptr &get_id_texture() const; + + /** + * Set the texture that maps pixels to entity IDs. + * + * Each pixel value in the texture corresponds to an entity ID. This + * mapping may be used for interacting with entities in the game world. + * + * @param id_texture ID texture. + */ + void set_id_texture(const std::shared_ptr &id_texture); + /** * Set the start position of a drag selection. * @@ -103,12 +133,12 @@ class Controller : public std::enable_shared_from_this { /** * Factions controllable by this controller. */ - std::unordered_set controlled_factions; + std::unordered_set controlled_factions; /** * ID of the currently active faction. */ - size_t active_faction_id; + gamestate::player_id_t active_faction_id; /** * Currently selected entities. @@ -120,6 +150,11 @@ class Controller : public std::enable_shared_from_this { */ std::vector> outqueue; + /** + * ID texture for interacting with game entities. + */ + std::shared_ptr id_texture; + /** * Start position of a drag selection. * diff --git a/libopenage/input/input_manager.cpp b/libopenage/input/input_manager.cpp index 4857863eb1..cdcdae600e 100644 --- a/libopenage/input/input_manager.cpp +++ b/libopenage/input/input_manager.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #include "input_manager.h" diff --git a/libopenage/input/input_manager.h b/libopenage/input/input_manager.h index 25b6315341..76d7ac1c4c 100644 --- a/libopenage/input/input_manager.h +++ b/libopenage/input/input_manager.h @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #pragma once @@ -16,7 +16,9 @@ namespace qtgui { class GuiInput; } -namespace openage::input { +namespace openage { + +namespace input { namespace camera { class Controller; @@ -149,7 +151,6 @@ class InputManager { */ bool process(const QEvent &ev); - private: /** * Process the (default) action for an input event. @@ -222,4 +223,5 @@ class InputManager { */ void setup_defaults(const std::shared_ptr &ctx); -} // namespace openage::input +} // namespace input +} // namespace openage 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/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index 9775b62d9b..0cd3122478 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -1,4 +1,4 @@ -// Copyright 2019-2024 the openage authors. See copying.md for legal info. +// Copyright 2019-2025 the openage authors. See copying.md for legal info. #include "presenter.h" @@ -243,13 +243,19 @@ void Presenter::init_input() { if (this->simulation) { log::log(INFO << "Loading game simulation controls"); - // TODO: Remove hardcoding + // TODO: Remove hardcoding for controlled/active factions + std::unordered_set controlled_factions{1, 2, 3, 4}; + gamestate::player_id_t active_faction_id = 1; auto game_controller = std::make_shared( - std::unordered_set{0, 1, 2, 3}, 0); + controlled_factions, active_faction_id); + auto engine_context = std::make_shared(); input::game::setup_defaults(engine_context, this->time_loop, this->simulation, this->camera); this->input_manager->set_game_controller(game_controller); input_ctx->set_game_bindings(engine_context); + + auto id_texture = this->world_renderer->get_id_texture(); + game_controller->set_id_texture(id_texture); } // attach GUI if it's initialized diff --git a/libopenage/renderer/opengl/framebuffer.cpp b/libopenage/renderer/opengl/framebuffer.cpp index 0ad46c1563..6dd39167d7 100644 --- a/libopenage/renderer/opengl/framebuffer.cpp +++ b/libopenage/renderer/opengl/framebuffer.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "framebuffer.h" @@ -30,26 +30,42 @@ GlFramebuffer::GlFramebuffer(const std::shared_ptr &context, glBindFramebuffer(GL_FRAMEBUFFER, handle); - std::vector drawBuffers; + std::vector draw_buffers; if (textures.empty()) { throw Error{ERR << "At least 1 texture must be assigned to texture framebuffer."}; } - size_t colorTextureCount = 0; + size_t color_texture_count = 0; + size_t depth_texture_count = 0; for (auto const &texture : textures) { - // TODO figure out attachment points from pixel formats - if (texture->get_info().get_format() == resources::pixel_format::depth24) { + auto fmt = texture->get_info().get_format(); + switch (fmt) { + case resources::pixel_format::depth24: + depth_texture_count += 1; + if (depth_texture_count > 1) { + log::log(WARN << "Framebuffer already has one depth texture attached. " + << "Assignment of additional depth texture ignored."); + break; + } glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture->get_handle(), 0); - } - else { - auto attachmentPoint = GL_COLOR_ATTACHMENT0 + colorTextureCount++; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentPoint, GL_TEXTURE_2D, texture->get_handle(), 0); - drawBuffers.push_back(attachmentPoint); + break; + case resources::pixel_format::r16ui: + case resources::pixel_format::r32ui: + case resources::pixel_format::rgba8: + case resources::pixel_format::rgb8: + case resources::pixel_format::bgr8: + case resources::pixel_format::rgba8ui: { + auto attachment_point = GL_COLOR_ATTACHMENT0 + color_texture_count++; + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_point, GL_TEXTURE_2D, texture->get_handle(), 0); + draw_buffers.push_back(attachment_point); + } break; + default: + throw Error{ERR << "Unsupported pixel format for framebuffer texture."}; } } - glDrawBuffers(drawBuffers.size(), drawBuffers.data()); + glDrawBuffers(draw_buffers.size(), draw_buffers.data()); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { throw Error(MSG(err) << "Could not create OpenGL framebuffer."); diff --git a/libopenage/renderer/opengl/render_target.cpp b/libopenage/renderer/opengl/render_target.cpp index b965dc58ea..a0e4862cf4 100644 --- a/libopenage/renderer/opengl/render_target.cpp +++ b/libopenage/renderer/opengl/render_target.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "render_target.h" @@ -22,8 +22,15 @@ GlRenderTarget::GlRenderTarget(const std::shared_ptr &context, type(gl_render_target_t::framebuffer), framebuffer({context, textures}), textures(textures) { - // TODO: Check if the textures are all the same size - this->size = this->textures.value().at(0)->get_info().get_size(); + // Check if the textures are all the same size + auto size = this->textures.value().at(0)->get_info().get_size(); + for (const auto &tex : this->textures.value()) { + if (tex->get_info().get_size() != size) { + throw Error{ERR << "All texture targets must be the same size."}; + } + } + + this->size = size; log::log(MSG(dbg) << "Created OpenGL render target for textures"); } diff --git a/libopenage/renderer/opengl/renderer.h b/libopenage/renderer/opengl/renderer.h index 751af7cacc..5d26d5cb87 100644 --- a/libopenage/renderer/opengl/renderer.h +++ b/libopenage/renderer/opengl/renderer.h @@ -33,31 +33,135 @@ class GlRenderer final : public Renderer { GlRenderer(const std::shared_ptr &ctx, const util::Vector2s &viewport_size); - std::shared_ptr add_texture(resources::Texture2dData const &) override; - std::shared_ptr add_texture(resources::Texture2dInfo const &) override; + /** + * Add a new texture from existing pixel data. + * + * @param data Texture data to upload to the GPU. + * + * @return Created texture object. + */ + std::shared_ptr add_texture(resources::Texture2dData const &data) override; - std::shared_ptr add_shader(std::vector const &) override; + /** + * Add a new texture from a texture information object. + * + * @param info Texture information describing texture size and format. + * + * @return Created texture object. + */ + std::shared_ptr add_texture(resources::Texture2dInfo const &info) override; - std::shared_ptr add_mesh_geometry(resources::MeshData const &) override; + /** + * Add a new shader program from shader source code. + * + * @param srcs Shader source codes to compile into a shader program. + * + * @return Created shader program. + */ + std::shared_ptr add_shader(std::vector const &srcs) override; + + /** + * Add a new geometry object from existing mesh data. + * + * Used for complex geometry with vertex attributes. + * + * @param mesh Mesh data to upload to the GPU. + * + * @return Created geometry object. + */ + std::shared_ptr add_mesh_geometry(resources::MeshData const &mesh) override; + + /** + * Add a new geometry object using a bufferless quad. + * + * Used for drawing a simple quad (rectangle). + * + * @return Created geometry object. + */ std::shared_ptr add_bufferless_quad() override; - std::shared_ptr add_render_pass(std::vector, const std::shared_ptr &) override; + /** + * Add a new render pass. + * + * Render passes group renderables that are drawn to the same target. + * + * @param renderables Renderables to be drawn in the pass. + * @param target Render target to draw into. + * + * @return Created render pass. + */ + std::shared_ptr add_render_pass(std::vector renderables, + const std::shared_ptr &target) override; - std::shared_ptr create_texture_target(std::vector> const &) override; + /** + * Add a render target that draws into the given texture attachments. + * + * Textures are attached in the order they appear in \p textures (for color attachments). + * Make sure to configure \p textures to match the layout of the output in the shader. + * + * @param textures Textures to attach to the framebuffer. + * + * @return Created render target. + */ + std::shared_ptr create_texture_target(std::vector> const &textures) override; + /** + * Get the render target for displaying on screen, i.e. targetting the window + * of the OpenGL context. + * + * @return Display target. + */ std::shared_ptr get_display_target() override; + /** + * Add a new uniform buffer from a uniform buffer information object. + * + * @param info Uniform buffer information describing the layout of the buffer. + * + * @return Created uniform buffer. + */ std::shared_ptr add_uniform_buffer(resources::UniformBufferInfo const &) override; - std::shared_ptr add_uniform_buffer(std::shared_ptr const &, - std::string const &) override; + /** + * Add a new uniform buffer from a shader program that has a uniform block. + * + * @param prog Shader program. The uniform block must be defined in the program. + * @param block_name Name of the block in the shader program. + * + * @return Created uniform buffer. + */ + std::shared_ptr add_uniform_buffer(std::shared_ptr const &prog, + std::string const &block_name) override; + + /** + * Get the current texture output of the display render target, i.e. the + * contents of the default framebuffer. + * + * @return Texture data from the display framebuffer. + */ resources::Texture2dData display_into_data() override; + /** + * Resize the display target to the given size. + * + * @param width New width. + * @param height New height. + */ void resize_display_target(size_t width, size_t height); + /** + * Check whether the graphics backend encountered any errors. + */ void check_error() override; - void render(const std::shared_ptr &) override; + /** + * Render the given render pass. + * + * Iterates over the renderables in the pass and draws them to the target. + * + * @param pass Render pass. + */ + void render(const std::shared_ptr &pass) override; private: /// Optimize the render pass by reordering stuff diff --git a/libopenage/renderer/opengl/texture.cpp b/libopenage/renderer/opengl/texture.cpp index 5e795bcd82..6a9d8d0538 100644 --- a/libopenage/renderer/opengl/texture.cpp +++ b/libopenage/renderer/opengl/texture.cpp @@ -1,9 +1,10 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #include "texture.h" #include +#include #include #include "../../datastructure/constexpr_map.h" @@ -86,7 +87,7 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, std::get<2>(fmt_in_out), nullptr); - // TODO these are outdated, use sampler settings + // TODO: these are outdated, use sampler settings glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -94,6 +95,56 @@ GlTexture2d::GlTexture2d(const std::shared_ptr &context, << size.first << "x" << size.second << ")"); } +void GlTexture2d::resize(size_t width, size_t height) { + auto prev_size = this->info.get_size(); + if (width == static_cast(prev_size.first) + and height == static_cast(prev_size.second)) { + // size is the same, no need to resize + log::log(MSG(dbg) << "Texture resize called, but size is unchanged (size: " + << prev_size.first << "x" << prev_size.second << ")"); + return; + } + + // only allow resizing for internal textures that are not created from + // image files + // TODO: maybe allow this for all textures? + if (this->info.get_image_path().has_value()) { + throw Error(MSG(err) << "Cannot resize a texture that was created from an image file."); + } + if (this->info.get_subtex_count() != 0) { + throw Error(MSG(err) << "Cannot resize a texture that has subtextures."); + } + + // create new info object + this->info = resources::Texture2dInfo(width, + height, + this->info.get_format(), + this->info.get_image_path(), + this->info.get_row_alignment()); + + glBindTexture(GL_TEXTURE_2D, *this->handle); + + auto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format()); + auto size = this->info.get_size(); + + // redefine the texture with the new size + glTexImage2D( + GL_TEXTURE_2D, + 0, + std::get<0>(fmt_in_out), + size.first, + size.second, + 0, + std::get<1>(fmt_in_out), + std::get<2>(fmt_in_out), + nullptr); + + // TODO: copy the old texture data into the new texture + + log::log(MSG(dbg) << "Resized OpenGL texture (size: " + << width << "x" << height << ")"); +} + resources::Texture2dData GlTexture2d::into_data() { auto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format()); std::vector data(this->info.get_data_size()); diff --git a/libopenage/renderer/opengl/texture.h b/libopenage/renderer/opengl/texture.h index fbc52d4423..8767ed9860 100644 --- a/libopenage/renderer/opengl/texture.h +++ b/libopenage/renderer/opengl/texture.h @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #pragma once @@ -23,6 +23,8 @@ class GlTexture2d final : public Texture2d GlTexture2d(const std::shared_ptr &context, resources::Texture2dInfo const &); + void resize(size_t width, size_t height) override; + resources::Texture2dData into_data() override; void upload(resources::Texture2dData const &) override; diff --git a/libopenage/renderer/resources/texture_data.cpp b/libopenage/renderer/resources/texture_data.cpp index fe11c19ce5..ddee5ad62f 100644 --- a/libopenage/renderer/resources/texture_data.cpp +++ b/libopenage/renderer/resources/texture_data.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #include "texture_data.h" @@ -153,10 +153,6 @@ const uint8_t *Texture2dData::get_data() const { void Texture2dData::store(const util::Path &file) const { log::log(MSG(info) << "Saving texture data to " << file); - if (this->info.get_format() != pixel_format::rgba8) { - throw Error(MSG(err) << "Storing 2D textures into files is unimplemented. PRs welcome :D"); - } - auto size = this->info.get_size(); QImage::Format pix_fmt; @@ -171,8 +167,11 @@ void Texture2dData::store(const util::Path &file) const { case pixel_format::rgba8: pix_fmt = QImage::Format_RGBA8888; break; + case pixel_format::r32ui: + pix_fmt = QImage::Format_RGBA8888; + break; default: - throw Error(MSG(err) << "Texture uses an unsupported format."); + throw Error(MSG(err) << "Texture uses an unsupported format for storing. PRs welcome :D"); } QImage image{this->data.data(), size.first, size.second, pix_fmt}; diff --git a/libopenage/renderer/resources/texture_data.h b/libopenage/renderer/resources/texture_data.h index 004a2bda1e..b6ac27acf6 100644 --- a/libopenage/renderer/resources/texture_data.h +++ b/libopenage/renderer/resources/texture_data.h @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #pragma once @@ -12,46 +12,72 @@ #include "texture_info.h" - namespace openage { namespace util { class Path; } namespace renderer::resources { -/// Stores 2D texture data in a CPU-accessible byte buffer. Provides methods for loading from -/// and storing onto disk, as well as sending to and receiving from graphics hardware. +/** + * Stores 2D texture data in a CPU-accessible byte buffer. Provides methods for loading from + * and storing onto disk, as well as sending to and receiving from graphics hardware. + */ class Texture2dData { public: - /// Create a texture from an image file. - /// @param path Path to the image file. - /// - /// Uses QImage internally. + /** + * Create a texture from an image file. + * + * Uses QImage internally. For supported image file types, + * see the QImage initialization in the engine. + * + * @param path Path to the image file. + */ Texture2dData(const util::Path &path); - /// Create a texture from info. - /// - /// Uses QImage internally. For supported image file types, - /// see the QImage initialization in the engine. + /** + * Create a texture from info. + * + * Uses QImage internally. For supported image file types, + * see the QImage initialization in the engine. + */ Texture2dData(Texture2dInfo const &info); - /// Construct by moving the information and raw texture data from somewhere else. + /** + * Construct by moving the information and raw texture data from somewhere else. + */ Texture2dData(Texture2dInfo const &info, std::vector &&data); - /// Flips the texture along the Y-axis and returns the flipped data with the same info. - /// Sometimes necessary when converting between storage modes. + /** + * Flips the texture along the Y-axis and returns the flipped data with the same info. + * Sometimes necessary when converting between storage modes. + */ Texture2dData flip_y(); - /// Returns the information describing this texture data. + /** + * Returns the information describing this texture data. + */ const Texture2dInfo &get_info() const; - /// Returns a pointer to the raw texture data, in row-major order. + /** + * Returns a pointer to the raw texture data, in row-major order. + */ const uint8_t *get_data() const; - /// Reads the pixel at the given position and casts it to the given type. - /// The texture is _not_ read as if it consisted of pixels of the given type, - /// but rather according to its original pixel format, so the coordinates - /// have to be specified according to that. + /** + * Reads the pixel at the given position and casts it to the given type. + * The texture is _not_ read as if it consisted of pixels of the given type, + * but rather according to its original pixel format, so the coordinates + * have to be specified according to that. + * + * @tparam T The type to cast the pixel to. + * + * @param x The x-coordinate of the pixel. + * @param y The y-coordinate of the pixel. + * + * @return The pixel value cast to the given type. + * + * @throws Error if the pixel position is outside the texture. + */ template T read_pixel(size_t x, size_t y) const { const uint8_t *data = this->data.data(); @@ -66,14 +92,22 @@ class Texture2dData { return *reinterpret_cast(data + off); } - /// Stores this texture data in the given file in the PNG format. + /** + * Stores this texture data in the given file in the PNG format. + * + * @param file The file path to store the texture data. + */ void store(const util::Path &file) const; private: - /// Information about this texture data. + /** + * Information about this texture data. + */ Texture2dInfo info; - /// The raw texture data. + /** + * The raw texture data. + */ std::vector data; }; diff --git a/libopenage/renderer/resources/texture_info.cpp b/libopenage/renderer/resources/texture_info.cpp index dfe3c573e7..e85086b721 100644 --- a/libopenage/renderer/resources/texture_info.cpp +++ b/libopenage/renderer/resources/texture_info.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "texture_info.h" @@ -71,6 +71,10 @@ size_t Texture2dInfo::get_subtex_count() const { return this->subtextures.size(); } +const std::vector &Texture2dInfo::get_subtextures() const { + return this->subtextures; +} + const Texture2dSubInfo &Texture2dInfo::get_subtex_info(size_t subidx) const { if (subidx < this->subtextures.size()) [[likely]] { return this->subtextures[subidx]; diff --git a/libopenage/renderer/resources/texture_info.h b/libopenage/renderer/resources/texture_info.h index 7884fe0f79..6b55d6f4b6 100644 --- a/libopenage/renderer/resources/texture_info.h +++ b/libopenage/renderer/resources/texture_info.h @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #pragma once @@ -151,6 +151,13 @@ class Texture2dInfo { */ size_t get_subtex_count() const; + /** + * Get the subtexture information for all subtextures. + * + * @return Subtexture information objects. + */ + const std::vector &get_subtextures() const; + /** * Get the subtexture information for a specific subtexture. * 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/hud/render_stage.cpp b/libopenage/renderer/stages/hud/render_stage.cpp index d5d7c5b83f..3bfeff6d77 100644 --- a/libopenage/renderer/stages/hud/render_stage.cpp +++ b/libopenage/renderer/stages/hud/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the openage authors. See copying.md for legal info. +// Copyright 2023-2025 the openage authors. See copying.md for legal info. #include "render_stage.h" @@ -92,8 +92,8 @@ void HudRenderStage::update() { } void HudRenderStage::resize(size_t width, size_t height) { - this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); - this->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24)); + this->output_texture->resize(width, height); + this->depth_texture->resize(width, height); auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture}); this->render_pass->set_target(fbo); 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/skybox/render_stage.cpp b/libopenage/renderer/stages/skybox/render_stage.cpp index b6fe47a7db..bc0a7c2f9d 100644 --- a/libopenage/renderer/stages/skybox/render_stage.cpp +++ b/libopenage/renderer/stages/skybox/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the openage authors. See copying.md for legal info. +// Copyright 2022-2025 the openage authors. See copying.md for legal info. #include "render_stage.h" @@ -10,10 +10,12 @@ #include "renderer/resources/shader_source.h" #include "renderer/resources/texture_info.h" #include "renderer/shader_program.h" +#include "renderer/texture.h" #include "renderer/uniform_input.h" #include "renderer/window.h" #include "util/path.h" + namespace openage::renderer::skybox { SkyboxRenderStage::SkyboxRenderStage(const std::shared_ptr &window, @@ -67,7 +69,7 @@ void SkyboxRenderStage::set_color(float r, float g, float b, float a) { } void SkyboxRenderStage::resize(size_t width, size_t height) { - this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); + this->output_texture->resize(width, height); auto fbo = this->renderer->create_texture_target({this->output_texture}); this->render_pass->set_target(fbo); 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/terrain/render_stage.cpp b/libopenage/renderer/stages/terrain/render_stage.cpp index 01376d886b..d902475587 100644 --- a/libopenage/renderer/stages/terrain/render_stage.cpp +++ b/libopenage/renderer/stages/terrain/render_stage.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2024 the openage authors. See copying.md for legal info. +// Copyright 2022-2025 the openage authors. See copying.md for legal info. #include "render_stage.h" @@ -13,6 +13,7 @@ #include "renderer/stages/terrain/chunk.h" #include "renderer/stages/terrain/mesh.h" #include "renderer/stages/terrain/model.h" +#include "renderer/texture.h" #include "renderer/window.h" #include "time/clock.h" @@ -88,8 +89,8 @@ void TerrainRenderStage::update() { } void TerrainRenderStage::resize(size_t width, size_t height) { - this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); - this->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24)); + this->output_texture->resize(width, height); + this->depth_texture->resize(width, height); auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture}); this->render_pass->set_target(fbo); 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/libopenage/renderer/stages/world/render_stage.cpp b/libopenage/renderer/stages/world/render_stage.cpp index eec13b602b..147706d8a5 100644 --- a/libopenage/renderer/stages/world/render_stage.cpp +++ b/libopenage/renderer/stages/world/render_stage.cpp @@ -109,14 +109,18 @@ void WorldRenderStage::update() { } void WorldRenderStage::resize(size_t width, size_t height) { - this->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8)); - this->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24)); - this->id_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::r32ui)); + this->output_texture->resize(width, height); + this->depth_texture->resize(width, height); + this->id_texture->resize(width, height); - auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture, this->id_texture}); + auto fbo = this->renderer->create_texture_target({this->output_texture, this->id_texture, this->depth_texture}); this->render_pass->set_target(fbo); } +const std::shared_ptr &WorldRenderStage::get_id_texture() const { + return this->id_texture; +} + void WorldRenderStage::initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir) { @@ -141,7 +145,7 @@ void WorldRenderStage::initialize_render_pass(size_t width, this->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src}); this->display_shader->bind_uniform_buffer("camera", this->camera->get_uniform_buffer()); - auto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture, this->id_texture}); + auto fbo = this->renderer->create_texture_target({this->output_texture, this->id_texture, this->depth_texture}); this->render_pass = this->renderer->add_render_pass({}, fbo); } diff --git a/libopenage/renderer/stages/world/render_stage.h b/libopenage/renderer/stages/world/render_stage.h index 7a0fe02a4c..bdd2a71d29 100644 --- a/libopenage/renderer/stages/world/render_stage.h +++ b/libopenage/renderer/stages/world/render_stage.h @@ -90,6 +90,13 @@ class WorldRenderStage { */ void resize(size_t width, size_t height); + /** + * Get the ID texture of the world renderer. + * + * @return ID texture. + */ + const std::shared_ptr &get_id_texture() const; + private: /** * Create the render pass for world drawing. diff --git a/libopenage/renderer/texture.h b/libopenage/renderer/texture.h index c2abdab2d2..3789fb4627 100644 --- a/libopenage/renderer/texture.h +++ b/libopenage/renderer/texture.h @@ -1,4 +1,4 @@ -// Copyright 2015-2024 the openage authors. See copying.md for legal info. +// Copyright 2015-2025 the openage authors. See copying.md for legal info. #pragma once @@ -8,28 +8,61 @@ namespace openage { namespace renderer { -/// An abstract base for a handle to a texture buffer allocated in graphics hardware. -/// Can be obtained by passing texture data to the renderer. +/** + * An abstract base for a handle to a texture buffer allocated in graphics hardware. + * Can be obtained by passing texture data to the renderer. + */ class Texture2d { public: virtual ~Texture2d(); - /// Returns the texture information. + /** + * Get the texture information. + * + * @return Information about the texture, such as size and format. + */ const resources::Texture2dInfo &get_info() const; - /// Copies this texture's data from graphics hardware into a CPU-accessible - /// Texture2dData buffer. + /** + * Resize the texture to a new size. + * + * Resizing is propagated to the GPU, so the texture may be reallocated depending on the + * underlying graphics API. The texture info is updated accordingly. + * + * Texture created from images cannot be resized. + * + * @param width New width of the texture. + * @param height New height of the texture. + */ + virtual void resize(size_t width, size_t height) = 0; + + /** + * Copies this texture's data from graphics hardware into a CPU-accessible + * Texture2dData buffer. + * + * @return A Texture2dData object containing the texture data. + */ virtual resources::Texture2dData into_data() = 0; - /// Uploads the provided data into the GPU texture storage. The format has - /// to match the format this Texture was originally created with. + /** + * Uploads the provided data into the GPU texture storage. The format has + * to match the format this Texture was originally created with. + * + * @param data The texture data to upload. + */ virtual void upload(resources::Texture2dData const &) = 0; protected: - /// Constructs the base with the given information. + /** + * Constructs the base with the given information. + * + * @param info Information about the texture, such as size and format. + */ Texture2d(const resources::Texture2dInfo &); - /// Information about the size, format, etc. of this texture. + /** + * Information about the size, format, etc. of this texture. + */ resources::Texture2dInfo info; }; diff --git a/libopenage/time/time.h b/libopenage/time/time.h index ceac0c6365..914854685e 100644 --- a/libopenage/time/time.h +++ b/libopenage/time/time.h @@ -1,4 +1,4 @@ -// Copyright 2017-2023 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #pragma once @@ -15,6 +15,12 @@ namespace openage::time { */ using time_t = util::FixedPoint; +/** + * Defines the type that is used for time durations. + * Same as time_t, but unsigned to cover the whole range of time_t. + */ +using time_duration_t = util::FixedPoint; + /** * Minimum time value. */ diff --git a/libopenage/util/fixed_point.h b/libopenage/util/fixed_point.h index c751238cdf..1c25457acb 100644 --- a/libopenage/util/fixed_point.h +++ b/libopenage/util/fixed_point.h @@ -18,13 +18,26 @@ namespace openage { namespace util { +/** + * Concept for fixed point storage types. + */ +template +concept StorageLike = std::is_integral::value; + +/** + * Concept for fixed point intermediate types. + */ +template +concept IntermediateLike = StorageLike; + + /** * Helper function that performs a left shift without causing undefined * behavior. * regular left-shift is undefined if amount >= bitwidth, * or amount >= bitwidth - 1 for signed integers. */ -template +template constexpr static typename std::enable_if<(amount + (std::is_signed::value ? 1 : 0) < sizeof(T) * CHAR_BIT), T>::type safe_shiftleft(T value) { @@ -38,14 +51,14 @@ constexpr static * behavior. * right-shift is usually undefined if amount >= bit size. */ -template +template constexpr static typename std::enable_if<(amount >= sizeof(T) * CHAR_BIT), T>::type safe_shiftright(T value) { return value < 0 ? -1 : 0; } -template +template constexpr static typename std::enable_if<(amount < sizeof(T) * CHAR_BIT), T>::type safe_shiftright(T value) { @@ -57,7 +70,7 @@ constexpr static * Helper function that performs either a safe shift-right (amount < 0), * or a safe shift-left (amount >= 0). */ -template +template constexpr static typename std::enable_if<(amount < 0), T>::type safe_shift(T value) { @@ -65,7 +78,7 @@ constexpr static } -template +template constexpr static typename std::enable_if<(amount >= 0), T>::type safe_shift(T value) { @@ -86,7 +99,7 @@ constexpr static * If you change this class, remember to update the gdb pretty printers * in etc/gdb_pretty/printers.py. */ -template +template class FixedPoint { public: using raw_type = int_type; @@ -267,13 +280,13 @@ class FixedPoint { /** * Factory function to get a fixed-point number from a fixed-point number of different type. */ - template other_fractional_bits)>::type * = nullptr> + template other_fractional_bits)>::type * = nullptr> static constexpr FixedPoint from_fixedpoint(const FixedPoint &other) { return FixedPoint::from_raw_value( safe_shift(static_cast(other.get_raw_value()))); } - template ::type * = nullptr> + template ::type * = nullptr> static constexpr FixedPoint from_fixedpoint(const FixedPoint &other) { return FixedPoint::from_raw_value( static_cast(other.get_raw_value() / safe_shiftleft(1))); @@ -383,12 +396,12 @@ class FixedPoint { return FixedPoint::this_type::from_raw_value(-this->raw_value); } - template + template constexpr double hypot(const FixedPoint rhs) { return std::hypot(this->to_double(), rhs.to_double()); } - template + template constexpr FixedPoint hypotfp(const FixedPoint rhs) { return FixedPoint(this->hypot(rhs)); } @@ -422,6 +435,45 @@ class FixedPoint { return *this; } + /** + * Get the absolute difference to another FixedPoint number. + * + * @param other Number to compare with. + * + * @return Absolute difference between \p this and \p other. + */ + constexpr same_type_but_unsigned abs_diff(const FixedPoint &other) const { + return FixedPoint::abs_diff(*this, other); + } + + /** + * Get the absolute difference between two FixedPoint numbers. + * + * Safe for signed types against integer overflow. + * + * @param first First number to compare with. + * @param second Second number to compare with. + * + * @return Absolute difference between \p first and \p second. + */ + static constexpr same_type_but_unsigned abs_diff(const FixedPoint &first, const FixedPoint &second) { + int_type diff; + int_type max = std::max(first.raw_value, second.raw_value); + int_type min = std::min(first.raw_value, second.raw_value); + + diff = max - min; + + // check if there was an overflow + if (diff < 0) { + // if there is an overflow, max is positive and min is negative + // we can safely cast max to unsigned and subtract min + unsigned_int_type u_diff = static_cast(max) - min; + return FixedPoint::same_type_but_unsigned::from_raw_value(u_diff); + } + + return FixedPoint::same_type_but_unsigned::from_raw_value(diff); + } + void swap(FixedPoint &rhs) { std::swap(this->raw_value, rhs.raw_value); } @@ -518,7 +570,7 @@ class FixedPoint { /** * FixedPoint + FixedPoint */ -template +template constexpr FixedPoint operator+(const FixedPoint &lhs, const FixedPoint &rhs) { return FixedPoint::from_raw_value(lhs.get_raw_value() + rhs.get_raw_value()); } @@ -526,7 +578,7 @@ constexpr FixedPoint operator+(const FixedPoint &lhs, /** * FixedPoint + double */ -template +template constexpr FixedPoint operator+(const FixedPoint &lhs, const double &rhs) { return FixedPoint{lhs} + FixedPoint::from_double(rhs); } @@ -534,7 +586,7 @@ constexpr FixedPoint operator+(const FixedPoint &lhs, /** * FixedPoint - FixedPoint */ -template +template constexpr FixedPoint operator-(const FixedPoint &lhs, const FixedPoint &rhs) { return FixedPoint::from_raw_value(lhs.get_raw_value() - rhs.get_raw_value()); } @@ -542,7 +594,7 @@ constexpr FixedPoint operator-(const FixedPoint &lhs, /** * FixedPoint - double */ -template +template constexpr FixedPoint operator-(const FixedPoint &lhs, const double &rhs) { return FixedPoint{lhs} - FixedPoint::from_double(rhs); } @@ -551,7 +603,7 @@ constexpr FixedPoint operator-(const FixedPoint &lhs, /** * FixedPoint * N */ -template +template typename std::enable_if::value, FixedPoint>::type constexpr operator*(const FixedPoint lhs, const N &rhs) { return FixedPoint::from_raw_value(lhs.get_raw_value() * rhs); } @@ -573,7 +625,7 @@ typename std::enable_if::value, FixedPoint>:: * * Use a larger intermediate type to prevent overflow */ -template +template constexpr FixedPoint operator*(const FixedPoint lhs, const FixedPoint rhs) { Inter ret = static_cast(lhs.get_raw_value()) * static_cast(rhs.get_raw_value()); ret >>= F; @@ -585,7 +637,7 @@ constexpr FixedPoint operator*(const FixedPoint lhs, c /** * FixedPoint / FixedPoint */ -template +template constexpr FixedPoint operator/(const FixedPoint lhs, const FixedPoint rhs) { Inter ret = div((static_cast(lhs.get_raw_value()) << F), static_cast(rhs.get_raw_value())); return FixedPoint::from_raw_value(static_cast(ret)); @@ -595,7 +647,7 @@ constexpr FixedPoint operator/(const FixedPoint lhs, c /** * FixedPoint / N */ -template +template constexpr FixedPoint operator/(const FixedPoint lhs, const N &rhs) { return FixedPoint::from_raw_value(div(lhs.get_raw_value(), static_cast(rhs))); } @@ -603,7 +655,7 @@ constexpr FixedPoint operator/(const FixedPoint lhs, c /** * FixedPoint % FixedPoint (modulo) */ -template +template constexpr FixedPoint operator%(const FixedPoint lhs, const FixedPoint rhs) { auto div = (lhs / rhs); auto n = div.to_int(); diff --git a/libopenage/util/fixed_point_test.cpp b/libopenage/util/fixed_point_test.cpp index b3690b8d15..ef1740c9af 100644 --- a/libopenage/util/fixed_point_test.cpp +++ b/libopenage/util/fixed_point_test.cpp @@ -83,6 +83,30 @@ void fixed_point() { TESTEQUALS(e < f, false); TESTEQUALS(e > f, true); + // test absolute difference + FixedPoint g(1.5); + FixedPoint h(2.5); + FixedPoint i = FixedPoint::min_value(); // -8.0 + FixedPoint j = FixedPoint::max_value(); // 7.9375 + + FixedPoint k = FixedPoint::abs_diff(g, h); + TESTEQUALS(k, 1); + k = FixedPoint::abs_diff(h, g); + TESTEQUALS(k, 1); + k = FixedPoint::abs_diff(g, i); + TESTEQUALS(k, 9.5); + k = FixedPoint::abs_diff(i, g); + TESTEQUALS(k, 9.5); + k = h.abs_diff(j); + TESTEQUALS(k, 5.4375); + k = j.abs_diff(h); + TESTEQUALS(k, 5.4375); + k = i.abs_diff(j); + FixedPoint max = FixedPoint::max_value(); + TESTEQUALS(k, max); + k = j.abs_diff(i); + TESTEQUALS(k, max); + // test the string I/O functions FString s; s << a; diff --git a/libopenage/util/fslike/directory.cpp b/libopenage/util/fslike/directory.cpp index 847ffe4d80..7432594b09 100644 --- a/libopenage/util/fslike/directory.cpp +++ b/libopenage/util/fslike/directory.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2024 the openage authors. See copying.md for legal info. +// Copyright 2017-2025 the openage authors. See copying.md for legal info. #include "directory.h" @@ -110,7 +110,7 @@ bool Directory::writable(const Path::parts_t &parts) { } const std::string path = this->resolve(parts_test); - return access(path.c_str(), W_OK); + return access(path.c_str(), W_OK) == 0; } diff --git a/openage/convert/entity_object/conversion/combined_sound.py b/openage/convert/entity_object/conversion/combined_sound.py index 027df77760..6f49fb8b74 100644 --- a/openage/convert/entity_object/conversion/combined_sound.py +++ b/openage/convert/entity_object/conversion/combined_sound.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. """ References a sound in the game that has to be converted. @@ -86,7 +86,7 @@ def get_relative_file_location(self) -> str: is expected to be in the modpack. """ if len(self._refs) > 1: - return f"../shared/sounds/{self.filename}.opus" + return f"../../shared/sounds/{self.filename}.opus" if len(self._refs) == 1: return f"./sounds/{self.filename}.opus" diff --git a/openage/convert/entity_object/conversion/combined_sprite.py b/openage/convert/entity_object/conversion/combined_sprite.py index 798c86796d..ad1a3eb3e5 100644 --- a/openage/convert/entity_object/conversion/combined_sprite.py +++ b/openage/convert/entity_object/conversion/combined_sprite.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. """ References a graphic in the game that has to be converted. @@ -94,7 +94,7 @@ def get_relative_sprite_location(self) -> str: is expected to be in the modpack. """ if len(self._refs) > 1: - return f"../shared/graphics/{self.filename}.sprite" + return f"../../shared/graphics/{self.filename}.sprite" if len(self._refs) == 1: return f"./graphics/{self.filename}.sprite" diff --git a/openage/convert/processor/conversion/aoc/ability_subprocessor.py b/openage/convert/processor/conversion/aoc/ability_subprocessor.py index 120541fe4c..19e0a6692e 100644 --- a/openage/convert/processor/conversion/aoc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/ability_subprocessor.py @@ -1,11 +1,11 @@ -# Copyright 2020-2024 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-public-methods,too-many-lines,too-many-locals # pylint: disable=too-many-branches,too-many-statements,too-many-arguments # pylint: disable=invalid-name # # TODO: -# pylint: disable=unused-argument,line-too-long +# pylint: disable=unused-argument,line-too-long,too-many-positional-arguments """ Derives and adds abilities to lines. Subroutine of the @@ -56,6 +56,45 @@ def active_transform_to_ability(line: GenieGameEntityGroup) -> ForwardRef: # TODO: Implement return None + @staticmethod + def activity_ability(line: GenieGameEntityGroup) -> ForwardRef: + """ + Adds the Activity ability to a line. + + :param line: Unit/Building line that gets the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :returns: The forward reference for the ability. + :rtype: ...dataformat.forward_ref.ForwardRef + """ + current_unit_id = line.get_head_unit_id() + + dataset = line.data + + name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version) + + game_entity_name = name_lookup_dict[current_unit_id][0] + + ability_ref = f"{game_entity_name}.Activity" + ability_raw_api_object = RawAPIObject(ability_ref, "Activity", dataset.nyan_api_objects) + ability_raw_api_object.add_raw_parent("engine.ability.type.Activity") + ability_location = ForwardRef(line, game_entity_name) + ability_raw_api_object.set_location(ability_location) + + # activity graph + if isinstance(line, GenieUnitLineGroup): + activity = dataset.pregen_nyan_objects["util.activity.types.Unit"].get_nyan_object() + + else: + activity = dataset.pregen_nyan_objects["util.activity.types.Default"].get_nyan_object() + + ability_raw_api_object.add_raw_member("graph", activity, "engine.ability.type.Activity") + + line.add_raw_api_object(ability_raw_api_object) + + ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id()) + + return ability_forward_ref + @staticmethod def apply_continuous_effect_ability( line: GenieGameEntityGroup, @@ -88,15 +127,9 @@ def apply_continuous_effect_ability( ability_name = command_lookup_dict[command_id][0] - if ranged: - ability_parent = "engine.ability.type.RangedContinuousEffect" - - else: - ability_parent = "engine.ability.type.ApplyContinuousEffect" - ability_ref = f"{game_entity_name}.{ability_name}" ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent(ability_parent) + ability_raw_api_object.add_raw_parent("engine.ability.type.ApplyContinuousEffect") ability_location = ForwardRef(line, game_entity_name) ability_raw_api_object.set_location(ability_location) @@ -234,12 +267,24 @@ def apply_continuous_effect_ability( properties, "engine.ability.Ability") + # Range if ranged: + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + # Min range min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.RangedContinuousEffect") + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") # Max range if command_id == 105: @@ -249,11 +294,18 @@ def apply_continuous_effect_ability( else: max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.RangedContinuousEffect") + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) # Effects + effects = None + allowed_types = None if command_id == 101: # Construct effects = AoCEffectSubprocessor.get_construct_effects(line, ability_ref) @@ -302,45 +354,6 @@ def apply_continuous_effect_ability( return ability_forward_ref - @staticmethod - def activity_ability(line: GenieGameEntityGroup) -> ForwardRef: - """ - Adds the Activity ability to a line. - - :param line: Unit/Building line that gets the ability. - :type line: ...dataformat.converter_object.ConverterObjectGroup - :returns: The forward reference for the ability. - :rtype: ...dataformat.forward_ref.ForwardRef - """ - current_unit_id = line.get_head_unit_id() - - dataset = line.data - - name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version) - - game_entity_name = name_lookup_dict[current_unit_id][0] - - ability_ref = f"{game_entity_name}.Activity" - ability_raw_api_object = RawAPIObject(ability_ref, "Activity", dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent("engine.ability.type.Activity") - ability_location = ForwardRef(line, game_entity_name) - ability_raw_api_object.set_location(ability_location) - - # activity graph - if isinstance(line, GenieUnitLineGroup): - activity = dataset.pregen_nyan_objects["util.activity.types.Unit"].get_nyan_object() - - else: - activity = dataset.pregen_nyan_objects["util.activity.types.Default"].get_nyan_object() - - ability_raw_api_object.add_raw_member("graph", activity, "engine.ability.type.Activity") - - line.add_raw_api_object(ability_raw_api_object) - - ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id()) - - return ability_forward_ref - @staticmethod def apply_discrete_effect_ability( line: GenieGameEntityGroup, @@ -376,18 +389,12 @@ def apply_discrete_effect_ability( ability_name = command_lookup_dict[command_id][0] - if ranged: - ability_parent = "engine.ability.type.RangedDiscreteEffect" - - else: - ability_parent = "engine.ability.type.ApplyDiscreteEffect" - if projectile == -1: ability_ref = f"{game_entity_name}.{ability_name}" ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent(ability_parent) + ability_raw_api_object.add_raw_parent("engine.ability.type.ApplyDiscreteEffect") ability_location = ForwardRef(line, game_entity_name) ability_raw_api_object.set_location(ability_location) @@ -398,7 +405,7 @@ def apply_discrete_effect_ability( f"{ability_name}") ability_raw_api_object = RawAPIObject( ability_ref, ability_name, dataset.nyan_api_objects) - ability_raw_api_object.add_raw_parent(ability_parent) + ability_raw_api_object.add_raw_parent("engine.ability.type.ApplyDiscreteEffect") ability_location = ForwardRef( line, f"{game_entity_name}.ShootProjectile.Projectile{projectile}" @@ -540,18 +547,35 @@ def apply_discrete_effect_ability( properties, "engine.ability.Ability") + # Range if ranged: + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + # Min range min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") # Max range max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) # Effects batch_ref = f"{ability_ref}.Batch" @@ -562,6 +586,7 @@ def apply_discrete_effect_ability( line.add_raw_api_object(batch_raw_api_object) + effects = None if command_id == 7: # Attack if projectile != 1: @@ -820,7 +845,7 @@ def collect_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def collision_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Collision ability to a line. @@ -2165,7 +2190,7 @@ def constructable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def create_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Create ability to a line. @@ -2239,7 +2264,7 @@ def create_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def death_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds a PassiveTransformTo ability to a line that is used to make entities die. @@ -2516,7 +2541,7 @@ def death_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def delete_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds a PassiveTransformTo ability to a line that is used to make entities die. @@ -2627,7 +2652,7 @@ def delete_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def despawn_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Despawn ability to a line. @@ -2778,7 +2803,7 @@ def despawn_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def drop_resources_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the DropResources ability to a line. @@ -2885,7 +2910,7 @@ def drop_resources_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def drop_site_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the DropSite ability to a line. @@ -2962,7 +2987,7 @@ def drop_site_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def enter_container_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the EnterContainer ability to a line. @@ -3029,7 +3054,7 @@ def enter_container_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def exchange_resources_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the ExchangeResources ability to a line. @@ -3096,7 +3121,7 @@ def exchange_resources_ability(line: GenieGameEntityGroup) -> ForwardRef: return abilities - @ staticmethod + @staticmethod def exit_container_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the ExitContainer ability to a line. @@ -3150,7 +3175,7 @@ def exit_container_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def game_entity_stance_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the GameEntityStance ability to a line. @@ -3237,7 +3262,7 @@ def game_entity_stance_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def formation_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Formation ability to a line. @@ -3320,7 +3345,7 @@ def formation_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def foundation_ability(line: GenieGameEntityGroup, terrain_id: int = -1) -> ForwardRef: """ Adds the Foundation abilities to a line. Optionally chooses the specified @@ -3364,7 +3389,7 @@ def foundation_ability(line: GenieGameEntityGroup, terrain_id: int = -1) -> Forw return ability_forward_ref - @ staticmethod + @staticmethod def gather_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Gather abilities to a line. Unlike the other methods, this @@ -3601,7 +3626,7 @@ def gather_ability(line: GenieGameEntityGroup) -> ForwardRef: return abilities - @ staticmethod + @staticmethod def harvestable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Harvestable ability to a line. @@ -3982,7 +4007,7 @@ def harvestable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def herd_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Herd ability to a line. @@ -4005,11 +4030,6 @@ def herd_ability(line: GenieGameEntityGroup) -> ForwardRef: ability_location = ForwardRef(line, game_entity_name) ability_raw_api_object.set_location(ability_location) - # Range - ability_raw_api_object.add_raw_member("range", - 3.0, - "engine.ability.type.Herd") - # Strength ability_raw_api_object.add_raw_member("strength", 0, @@ -4028,13 +4048,45 @@ def herd_ability(line: GenieGameEntityGroup) -> ForwardRef: [], "engine.ability.type.Herd") + properties = {} + + # Ranged property + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + + property_raw_api_object.add_raw_member("min_range", + 0.0, + "engine.ability.property.type.Ranged") + property_raw_api_object.add_raw_member("max_range", + 3.0, # hardcoded + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) + + # TODO: Animated property + # animation seems to be hardcoded? + + ability_raw_api_object.add_raw_member("properties", + properties, + "engine.ability.Ability") + line.add_raw_api_object(ability_raw_api_object) ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id()) return ability_forward_ref - @ staticmethod + @staticmethod def herdable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Herdable ability to a line. @@ -4074,7 +4126,7 @@ def herdable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def idle_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Idle ability to a line. @@ -4179,7 +4231,7 @@ def idle_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def live_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Live ability to a line. @@ -4285,7 +4337,7 @@ def live_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def los_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the LineOfSight ability to a line. @@ -4344,7 +4396,7 @@ def los_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def move_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Move ability to a line. @@ -4543,7 +4595,7 @@ def move_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def move_projectile_ability(line: GenieGameEntityGroup, position: int = -1) -> ForwardRef: """ Adds the Move ability to a projectile of the specified line. @@ -4640,7 +4692,7 @@ def move_projectile_ability(line: GenieGameEntityGroup, position: int = -1) -> F return ability_forward_ref - @ staticmethod + @staticmethod def named_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Named ability to a line. @@ -4732,7 +4784,7 @@ def named_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def overlay_terrain_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the OverlayTerrain to a line. @@ -4773,7 +4825,7 @@ def overlay_terrain_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def pathable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Pathable ability to a line. @@ -4820,7 +4872,7 @@ def pathable_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def production_queue_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the ProductionQueue ability to a line. @@ -4880,7 +4932,7 @@ def production_queue_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def projectile_ability(line: GenieGameEntityGroup, position: int = 0) -> ForwardRef: """ Adds a Projectile ability to projectiles in a line. Which projectile should @@ -4991,7 +5043,7 @@ def projectile_ability(line: GenieGameEntityGroup, position: int = 0) -> Forward return ability_forward_ref - @ staticmethod + @staticmethod def provide_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the ProvideContingent ability to a line. @@ -5070,7 +5122,7 @@ def provide_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def rally_point_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the RallyPoint ability to a line. @@ -5099,7 +5151,7 @@ def rally_point_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def regenerate_attribute_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the RegenerateAttribute ability to a line. @@ -5180,7 +5232,7 @@ def regenerate_attribute_ability(line: GenieGameEntityGroup) -> ForwardRef: return [ability_forward_ref] - @ staticmethod + @staticmethod def regenerate_resource_spot_ability(line: GenieGameEntityGroup) -> None: """ Adds the RegenerateResourceSpot ability to a line. @@ -5192,7 +5244,7 @@ def regenerate_resource_spot_ability(line: GenieGameEntityGroup) -> None: """ # Unused in AoC - @ staticmethod + @staticmethod def remove_storage_ability(line) -> ForwardRef: """ Adds the RemoveStorage ability to a line. @@ -5242,7 +5294,7 @@ def remove_storage_ability(line) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def restock_ability(line: GenieGameEntityGroup, restock_target_id: int) -> ForwardRef: """ Adds the Restock ability to a line. @@ -5380,7 +5432,7 @@ def restock_ability(line: GenieGameEntityGroup, restock_target_id: int) -> Forwa return ability_forward_ref - @ staticmethod + @staticmethod def research_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Research ability to a line. @@ -5456,7 +5508,7 @@ def research_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def resistance_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Resistance ability to a line. @@ -5506,7 +5558,7 @@ def resistance_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the ResourceStorage ability to a line. @@ -5595,6 +5647,7 @@ def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: # The unit uses no gathering command or we don't recognize it continue + container_name = None if line.is_gatherer(): gatherer_unit_id = gatherer.get_id() if gatherer_unit_id not in gather_lookup_dict: @@ -5621,6 +5674,7 @@ def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: "engine.util.storage.ResourceContainer") # Carry capacity + carry_capacity = None if line.is_gatherer(): carry_capacity = gatherer["resource_capacity"].value @@ -5755,7 +5809,7 @@ def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def selectable_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds Selectable abilities to a line. Units will get two of these, @@ -5976,7 +6030,7 @@ def selectable_ability(line: GenieGameEntityGroup) -> ForwardRef: return abilities - @ staticmethod + @staticmethod def send_back_to_task_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the SendBackToTask ability to a line. @@ -6017,7 +6071,7 @@ def send_back_to_task_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> ForwardRef: """ Adds the ShootProjectile ability to a line. @@ -6051,6 +6105,31 @@ def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> For # Ability properties properties = {} + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + + min_range = current_unit["weapon_range_min"].value + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") + max_range = current_unit["weapon_range_max"].value + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) + # Animation ability_animation_id = current_unit["attack_sprite_id"].value if ability_animation_id > -1: @@ -6178,17 +6257,6 @@ def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> For max_projectiles, "engine.ability.type.ShootProjectile") - # Range - min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.ShootProjectile") - - max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.ShootProjectile") - # Reload time and delay reload_time = current_unit["attack_speed"].value ability_raw_api_object.add_raw_member("reload_time", @@ -6276,7 +6344,7 @@ def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> For return ability_forward_ref - @ staticmethod + @staticmethod def stop_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Stop ability to a line. @@ -6330,7 +6398,7 @@ def stop_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def storage_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Storage ability to a line. @@ -6767,7 +6835,7 @@ def storage_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def terrain_requirement_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the TerrainRequirement to a line. @@ -6820,7 +6888,7 @@ def terrain_requirement_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def trade_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Trade ability to a line. @@ -6884,7 +6952,7 @@ def trade_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def trade_post_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the TradePost ability to a line. @@ -6951,7 +7019,7 @@ def trade_post_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def transfer_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the TransferStorage ability to a line. @@ -7030,7 +7098,7 @@ def transfer_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def turn_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Turn ability to a line. @@ -7104,7 +7172,7 @@ def turn_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def use_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the UseContingent ability to a line. @@ -7180,7 +7248,7 @@ def use_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def visibility_ability(line: GenieGameEntityGroup) -> ForwardRef: """ Adds the Visibility ability to a line. @@ -7299,7 +7367,7 @@ def visibility_ability(line: GenieGameEntityGroup) -> ForwardRef: return ability_forward_ref - @ staticmethod + @staticmethod def create_animation( line: GenieGameEntityGroup, animation_id: int, @@ -7355,7 +7423,7 @@ def create_animation( return animation_forward_ref - @ staticmethod + @staticmethod def create_civ_animation( line: GenieGameEntityGroup, civ_group: GenieCivilizationGroup, @@ -7465,7 +7533,7 @@ def create_civ_animation( [wrapper_forward_ref]) civ_group.add_raw_member_push(push_object) - @ staticmethod + @staticmethod def create_sound( line: GenieGameEntityGroup, sound_id: int, @@ -7530,7 +7598,7 @@ def create_sound( return sound_forward_ref - @ staticmethod + @staticmethod def create_language_strings( line: GenieGameEntityGroup, string_id: int, diff --git a/openage/convert/processor/conversion/aoc/pregen_processor.py b/openage/convert/processor/conversion/aoc/pregen_processor.py index 48eb8b97e7..963fa22a79 100644 --- a/openage/convert/processor/conversion/aoc/pregen_processor.py +++ b/openage/convert/processor/conversion/aoc/pregen_processor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-lines,too-many-locals,too-many-statements # @@ -91,13 +91,19 @@ def generate_activities( start_parent = "engine.util.activity.node.type.Start" end_parent = "engine.util.activity.node.type.End" ability_parent = "engine.util.activity.node.type.Ability" + task_parent = "engine.util.activity.node.type.Task" 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" + cond_ability_parent = "engine.util.activity.condition.type.AbilityUsable" + cond_queue_parent = "engine.util.activity.condition.type.CommandInQueue" + cond_target_parent = "engine.util.activity.condition.type.TargetInRange" + cond_command_switch_parent = ( + "engine.util.activity.switch_condition.type.NextCommand" + ) # ======================================================================= # Default (Start -> Ability(Idle) -> End) @@ -241,7 +247,7 @@ def generate_activities( condition_raw_api_object = RawAPIObject(condition_ref_in_modpack, "CommandInQueue", api_objects) condition_raw_api_object.set_location(queue_forward_ref) - condition_raw_api_object.add_raw_parent(condition_queue_parent) + condition_raw_api_object.add_raw_parent(cond_queue_parent) branch_forward_ref = ForwardRef(pregen_converter_group, "util.activity.types.Unit.BranchCommand") @@ -274,38 +280,217 @@ 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(cond_command_switch_parent) + ability_check_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.ApplyEffectUsableCheck") move_forward_ref = ForwardRef(pregen_converter_group, "util.activity.types.Unit.Move") + next_nodes_lookup = { + api_objects["engine.util.command.type.ApplyEffect"]: ability_check_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, + cond_command_switch_parent) pregen_converter_group.add_raw_api_object(condition_raw_api_object) pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object}) + # Ability usability gate + ability_check_ref_in_modpack = "util.activity.types.Unit.ApplyEffectUsableCheck" + ability_check_raw_api_object = RawAPIObject(ability_check_ref_in_modpack, + "ApplyEffectUsableCheck", api_objects) + ability_check_raw_api_object.set_location(unit_forward_ref) + ability_check_raw_api_object.add_raw_parent(xor_parent) + + condition_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.ApplyEffectUsable") + ability_check_raw_api_object.add_raw_member("next", + [condition_forward_ref], + xor_parent) + pop_command_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.PopCommand") + ability_check_raw_api_object.add_raw_member("default", + pop_command_forward_ref, + xor_parent) + + pregen_converter_group.add_raw_api_object(ability_check_raw_api_object) + pregen_nyan_objects.update({ability_check_ref_in_modpack: ability_check_raw_api_object}) + + # Apply effect usability condition + apply_effect_ref_in_modpack = "util.activity.types.Unit.ApplyEffectUsable" + apply_effect_raw_api_object = RawAPIObject(apply_effect_ref_in_modpack, + "ApplyEffectUsable", api_objects) + apply_effect_raw_api_object.set_location(unit_forward_ref) + apply_effect_raw_api_object.add_raw_parent(cond_ability_parent) + + target_in_range_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.RangeCheck") + apply_effect_raw_api_object.add_raw_member("next", + target_in_range_forward_ref, + condition_parent) + apply_effect_raw_api_object.add_raw_member( + "ability", + api_objects["engine.ability.type.ApplyDiscreteEffect"], + cond_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}) + + # Pop command task + pop_command_ref_in_modpack = "util.activity.types.Unit.PopCommand" + pop_command_raw_api_object = RawAPIObject(pop_command_ref_in_modpack, + "PopCommand", api_objects) + pop_command_raw_api_object.set_location(unit_forward_ref) + pop_command_raw_api_object.add_raw_parent(task_parent) + + idle_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.Idle") + pop_command_raw_api_object.add_raw_member("next", idle_forward_ref, + task_parent) + pop_command_raw_api_object.add_raw_member( + "task", + api_objects["engine.util.activity.task.type.PopCommandQueue"], + task_parent + ) + + pregen_converter_group.add_raw_api_object(pop_command_raw_api_object) + pregen_nyan_objects.update({pop_command_ref_in_modpack: pop_command_raw_api_object}) + + # Target in range gate + range_check_ref_in_modpack = "util.activity.types.Unit.RangeCheck" + range_check_raw_api_object = RawAPIObject(range_check_ref_in_modpack, + "RangeCheck", api_objects) + range_check_raw_api_object.set_location(unit_forward_ref) + range_check_raw_api_object.add_raw_parent(xor_parent) + + target_in_range_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.TargetInRange") + range_check_raw_api_object.add_raw_member("next", + [target_in_range_forward_ref], + xor_parent) + move_to_target_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.MoveToTarget") + range_check_raw_api_object.add_raw_member("default", + move_to_target_forward_ref, + xor_parent) + + pregen_converter_group.add_raw_api_object(range_check_raw_api_object) + pregen_nyan_objects.update({range_check_ref_in_modpack: range_check_raw_api_object}) + + # Target in range condition + target_in_range_ref_in_modpack = "util.activity.types.Unit.TargetInRange" + target_in_range_raw_api_object = RawAPIObject(target_in_range_ref_in_modpack, + "TargetInRange", api_objects) + target_in_range_raw_api_object.set_location(unit_forward_ref) + target_in_range_raw_api_object.add_raw_parent(cond_target_parent) + + apply_effect_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.ApplyEffect") + target_in_range_raw_api_object.add_raw_member("next", + apply_effect_forward_ref, + condition_parent) + + target_in_range_raw_api_object.add_raw_member( + "ability", + api_objects["engine.ability.type.ApplyDiscreteEffect"], + cond_target_parent + ) + + pregen_converter_group.add_raw_api_object(target_in_range_raw_api_object) + pregen_nyan_objects.update( + {target_in_range_ref_in_modpack: target_in_range_raw_api_object} + ) + + # Move to target task + move_to_target_ref_in_modpack = "util.activity.types.Unit.MoveToTarget" + move_to_target_raw_api_object = RawAPIObject(move_to_target_ref_in_modpack, + "MoveToTarget", api_objects) + move_to_target_raw_api_object.set_location(unit_forward_ref) + move_to_target_raw_api_object.add_raw_parent(task_parent) + + wait_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.WaitMoveToTarget") + move_to_target_raw_api_object.add_raw_member("next", + wait_forward_ref, + task_parent) + move_to_target_raw_api_object.add_raw_member( + "task", + api_objects["engine.util.activity.task.type.MoveToTarget"], + task_parent + ) + + pregen_converter_group.add_raw_api_object(move_to_target_raw_api_object) + pregen_nyan_objects.update( + {move_to_target_ref_in_modpack: move_to_target_raw_api_object} + ) + + # Wait for MoveToTarget task (for movement to finish) + wait_ref_in_modpack = "util.activity.types.Unit.WaitMoveToTarget" + wait_raw_api_object = RawAPIObject(wait_ref_in_modpack, + "WaitMoveToTarget", api_objects) + wait_raw_api_object.set_location(unit_forward_ref) + wait_raw_api_object.add_raw_parent(xor_event_parent) + + wait_finish = api_objects["engine.util.activity.event.type.WaitAbility"] + wait_command = api_objects["engine.util.activity.event.type.CommandInQueue"] + range_check_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.RangeCheck") + branch_forward_ref = ForwardRef(pregen_converter_group, + "util.activity.types.Unit.BranchCommand") + wait_raw_api_object.add_raw_member("next", + { + wait_finish: range_check_forward_ref, + wait_command: branch_forward_ref + }, + xor_event_parent) + + pregen_converter_group.add_raw_api_object(wait_raw_api_object) + pregen_nyan_objects.update({wait_ref_in_modpack: wait_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.WaitAbility") + 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, @@ -314,7 +499,7 @@ def generate_activities( move_raw_api_object.add_raw_parent(ability_parent) wait_forward_ref = ForwardRef(pregen_converter_group, - "util.activity.types.Unit.Wait") + "util.activity.types.Unit.WaitAbility") move_raw_api_object.add_raw_member("next", wait_forward_ref, ability_parent) move_raw_api_object.add_raw_member("ability", @@ -324,8 +509,8 @@ def generate_activities( pregen_converter_group.add_raw_api_object(move_raw_api_object) pregen_nyan_objects.update({move_ref_in_modpack: move_raw_api_object}) - # Wait (for Move or Command) - wait_ref_in_modpack = "util.activity.types.Unit.Wait" + # Wait after ability usage (for Move/ApplyEffect or new command) + wait_ref_in_modpack = "util.activity.types.Unit.WaitAbility" wait_raw_api_object = RawAPIObject(wait_ref_in_modpack, "Wait", api_objects) wait_raw_api_object.set_location(unit_forward_ref) @@ -336,8 +521,6 @@ def generate_activities( wait_raw_api_object.add_raw_member("next", { wait_finish: idle_forward_ref, - # TODO: don't go back to move, go to xor gate that - # branches depending on command wait_command: branch_forward_ref }, xor_event_parent) diff --git a/openage/convert/processor/conversion/aoc/upgrade_ability_subprocessor.py b/openage/convert/processor/conversion/aoc/upgrade_ability_subprocessor.py index f00814db31..7fd1cf0e36 100644 --- a/openage/convert/processor/conversion/aoc/upgrade_ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/upgrade_ability_subprocessor.py @@ -1,10 +1,10 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,invalid-name # pylint: disable=too-many-public-methods,too-many-branches,too-many-arguments # # TODO: -# pylint: disable=unused-argument,line-too-long +# pylint: disable=unused-argument,line-too-long,too-many-positional-arguments """ Creates upgrade patches for abilities. @@ -84,16 +84,71 @@ def apply_continuous_effect_ability( # Command types Heal, Construct, Repair are not upgraded by lines - diff_min_range = None - diff_max_range = None - if not data_changed and ranged: + if ranged: diff_min_range = diff["weapon_range_min"] diff_max_range = diff["weapon_range_max"] + if any(not isinstance(value, NoDiffMember) for value in ( diff_min_range, diff_max_range )): - data_changed = True + patch_target_ref = f"{game_entity_name}.{ability_name}.Ranged" + patch_target_forward_ref = ForwardRef(line, patch_target_ref) + + # Wrapper + wrapper_name = f"Change{game_entity_name}{ability_name}RangedWrapper" + wrapper_ref = f"{container_obj_ref}.{wrapper_name}" + wrapper_raw_api_object = RawAPIObject(wrapper_ref, + wrapper_name, + dataset.nyan_api_objects) + wrapper_raw_api_object.add_raw_parent("engine.util.patch.Patch") + + if isinstance(line, GenieBuildingLineGroup): + wrapper_raw_api_object.set_location(("data/game_entity/generic/" + f"{name_lookup_dict[head_unit_id][1]}/")) + wrapper_raw_api_object.set_filename(f"{tech_lookup_dict[tech_id][1]}_upgrade") + + else: + wrapper_raw_api_object.set_location(ForwardRef(converter_group, + container_obj_ref)) + + # Nyan patch + nyan_patch_name = f"Change{game_entity_name}{ability_name}Ranged" + nyan_patch_ref = ForwardRef(line, nyan_patch_name) + nyan_patch_location = ForwardRef(converter_group, wrapper_ref) + nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref, + nyan_patch_name, + dataset.nyan_api_objects, + nyan_patch_location) + nyan_patch_raw_api_object.add_raw_parent("engine.util.patch.NyanPatch") + nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref) + + if not isinstance(diff_min_range, NoDiffMember): + min_range = diff_min_range.value + nyan_patch_raw_api_object.add_raw_patch_member( + "min_range", + min_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + if not isinstance(diff_max_range, NoDiffMember): + max_range = diff_max_range.value + nyan_patch_raw_api_object.add_raw_patch_member( + "max_range", + max_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) + wrapper_raw_api_object.add_raw_member("patch", + patch_forward_ref, + "engine.util.patch.Patch") + + converter_group.add_raw_api_object(wrapper_raw_api_object) + converter_group.add_raw_api_object(nyan_patch_raw_api_object) + + wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref) + patches.append(wrapper_forward_ref) if not isinstance(diff_animation, NoDiffMember): diff_animation_id = diff_animation.value @@ -195,23 +250,6 @@ def apply_continuous_effect_ability( "engine.ability.type.ApplyContinuousEffect", MemberOperator.ASSIGN) - if ranged: - if not isinstance(diff_min_range, NoDiffMember): - min_range = diff_min_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("min_range", - min_range, - "engine.ability.type.RangedContinuousEffect", - MemberOperator.ADD) - - if not isinstance(diff_max_range, NoDiffMember): - max_range = diff_max_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("max_range", - max_range, - "engine.ability.type.RangedContinuousEffect", - MemberOperator.ADD) - patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) wrapper_raw_api_object.add_raw_member("patch", patch_forward_ref, @@ -270,16 +308,71 @@ def apply_discrete_effect_ability( diff_frame_delay)): data_changed = True - diff_min_range = None - diff_max_range = None if ranged: diff_min_range = diff["weapon_range_min"] diff_max_range = diff["weapon_range_max"] + if any(not isinstance(value, NoDiffMember) for value in ( diff_min_range, diff_max_range )): - data_changed = True + patch_target_ref = f"{game_entity_name}.{ability_name}.Ranged" + patch_target_forward_ref = ForwardRef(line, patch_target_ref) + + # Wrapper + wrapper_name = f"Change{game_entity_name}{ability_name}RangedWrapper" + wrapper_ref = f"{container_obj_ref}.{wrapper_name}" + wrapper_raw_api_object = RawAPIObject(wrapper_ref, + wrapper_name, + dataset.nyan_api_objects) + wrapper_raw_api_object.add_raw_parent("engine.util.patch.Patch") + + if isinstance(line, GenieBuildingLineGroup): + wrapper_raw_api_object.set_location(("data/game_entity/generic/" + f"{name_lookup_dict[head_unit_id][1]}/")) + wrapper_raw_api_object.set_filename(f"{tech_lookup_dict[tech_id][1]}_upgrade") + + else: + wrapper_raw_api_object.set_location(ForwardRef(converter_group, + container_obj_ref)) + + # Nyan patch + nyan_patch_name = f"Change{game_entity_name}{ability_name}Ranged" + nyan_patch_ref = ForwardRef(line, nyan_patch_name) + nyan_patch_location = ForwardRef(converter_group, wrapper_ref) + nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref, + nyan_patch_name, + dataset.nyan_api_objects, + nyan_patch_location) + nyan_patch_raw_api_object.add_raw_parent("engine.util.patch.NyanPatch") + nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref) + + if not isinstance(diff_min_range, NoDiffMember): + min_range = diff_min_range.value + nyan_patch_raw_api_object.add_raw_patch_member( + "min_range", + min_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + if not isinstance(diff_max_range, NoDiffMember): + max_range = diff_max_range.value + nyan_patch_raw_api_object.add_raw_patch_member( + "max_range", + max_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) + wrapper_raw_api_object.add_raw_member("patch", + patch_forward_ref, + "engine.util.patch.Patch") + + converter_group.add_raw_api_object(wrapper_raw_api_object) + converter_group.add_raw_api_object(nyan_patch_raw_api_object) + + wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref) + patches.append(wrapper_forward_ref) if not isinstance(diff_animation, NoDiffMember): diff_animation_id = diff_animation.value @@ -389,23 +482,6 @@ def apply_discrete_effect_ability( "engine.ability.type.ApplyDiscreteEffect", MemberOperator.ASSIGN) - if ranged: - if not isinstance(diff_min_range, NoDiffMember): - min_range = diff_min_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("min_range", - min_range, - "engine.ability.type.RangedApplyDiscreteEffect", - MemberOperator.ADD) - - if not isinstance(diff_max_range, NoDiffMember): - max_range = diff_max_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("max_range", - max_range, - "engine.ability.type.RangedApplyDiscreteEffect", - MemberOperator.ADD) - patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) wrapper_raw_api_object.add_raw_member("patch", patch_forward_ref, @@ -1409,7 +1485,7 @@ def shoot_projectile_ability( diff: ConverterObject = None ) -> list[ForwardRef]: """ - Creates a patch for the Selectable ability of a line. + Creates a patch for the ShootProjectile ability of a line. :param converter_group: Group that gets the patch. :type converter_group: ...dataformat.converter_object.ConverterObjectGroup @@ -1436,34 +1512,91 @@ def shoot_projectile_ability( ability_name = command_lookup_dict[command_id][0] data_changed = False - if diff: - diff_animation = diff["attack_sprite_id"] - diff_comm_sound = diff["command_sound_id"] - diff_min_projectiles = diff["projectile_min_count"] - diff_max_projectiles = diff["projectile_max_count"] - diff_min_range = diff["weapon_range_min"] - diff_max_range = diff["weapon_range_min"] - diff_reload_time = diff["attack_speed"] - # spawn delay also depends on animation - diff_spawn_delay = diff["frame_delay"] - diff_spawn_area_offsets = diff["weapon_offset"] - diff_spawn_area_width = diff["projectile_spawning_area_width"] - diff_spawn_area_height = diff["projectile_spawning_area_length"] - diff_spawn_area_randomness = diff["projectile_spawning_area_randomness"] - if any(not isinstance(value, NoDiffMember) for value in ( - diff_min_projectiles, - diff_max_projectiles, - diff_min_range, - diff_max_range, - diff_reload_time, - diff_spawn_delay, - diff_spawn_area_offsets, - diff_spawn_area_width, - diff_spawn_area_height, - diff_spawn_area_randomness - )): - data_changed = True + diff_animation = diff["attack_sprite_id"] + diff_comm_sound = diff["command_sound_id"] + diff_min_projectiles = diff["projectile_min_count"] + diff_max_projectiles = diff["projectile_max_count"] + diff_min_range = diff["weapon_range_min"] + diff_max_range = diff["weapon_range_min"] + diff_reload_time = diff["attack_speed"] + # spawn delay also depends on animation + diff_spawn_delay = diff["frame_delay"] + diff_spawn_area_offsets = diff["weapon_offset"] + diff_spawn_area_width = diff["projectile_spawning_area_width"] + diff_spawn_area_height = diff["projectile_spawning_area_length"] + diff_spawn_area_randomness = diff["projectile_spawning_area_randomness"] + + if any(not isinstance(value, NoDiffMember) for value in ( + diff_min_projectiles, + diff_max_projectiles, + diff_reload_time, + diff_spawn_delay, + diff_spawn_area_offsets, + diff_spawn_area_width, + diff_spawn_area_height, + diff_spawn_area_randomness + )): + data_changed = True + + if any(not isinstance(value, NoDiffMember) for value in ( + diff_min_range, + diff_max_range + )): + patch_target_ref = f"{game_entity_name}.{ability_name}.Ranged" + patch_target_forward_ref = ForwardRef(line, patch_target_ref) + + # Wrapper + wrapper_name = f"Change{game_entity_name}{ability_name}RangedWrapper" + wrapper_ref = f"{container_obj_ref}.{wrapper_name}" + wrapper_raw_api_object = RawAPIObject(wrapper_ref, + wrapper_name, + dataset.nyan_api_objects) + wrapper_raw_api_object.add_raw_parent("engine.util.patch.Patch") + + if isinstance(line, GenieBuildingLineGroup): + wrapper_raw_api_object.set_location(("data/game_entity/generic/" + f"{name_lookup_dict[head_unit_id][1]}/")) + wrapper_raw_api_object.set_filename(f"{tech_lookup_dict[tech_id][1]}_upgrade") + + else: + wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref)) + + # Nyan patch + nyan_patch_name = f"Change{game_entity_name}{ability_name}Ranged" + nyan_patch_ref = ForwardRef(line, nyan_patch_name) + nyan_patch_location = ForwardRef(converter_group, wrapper_ref) + nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref, + nyan_patch_name, + dataset.nyan_api_objects, + nyan_patch_location) + nyan_patch_raw_api_object.add_raw_parent("engine.util.patch.NyanPatch") + nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref) + + if not isinstance(diff_min_range, NoDiffMember): + min_range = diff_min_range.value + nyan_patch_raw_api_object.add_raw_patch_member("min_range", + min_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + if not isinstance(diff_max_range, NoDiffMember): + max_range = diff_max_range.value + nyan_patch_raw_api_object.add_raw_patch_member("max_range", + max_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) + wrapper_raw_api_object.add_raw_member("patch", + patch_forward_ref, + "engine.util.patch.Patch") + + converter_group.add_raw_api_object(wrapper_raw_api_object) + converter_group.add_raw_api_object(nyan_patch_raw_api_object) + + wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref) + patches.append(wrapper_forward_ref) if not isinstance(diff_animation, NoDiffMember): diff_animation_id = diff_animation.value @@ -1592,20 +1725,6 @@ def shoot_projectile_ability( "engine.ability.type.ShootProjectile", MemberOperator.ADD) - if not isinstance(diff_min_range, NoDiffMember): - min_range = diff_min_range.value - nyan_patch_raw_api_object.add_raw_patch_member("min_range", - min_range, - "engine.ability.type.ShootProjectile", - MemberOperator.ADD) - - if not isinstance(diff_max_range, NoDiffMember): - max_range = diff_max_range.value - nyan_patch_raw_api_object.add_raw_patch_member("max_range", - max_range, - "engine.ability.type.ShootProjectile", - MemberOperator.ADD) - if not isinstance(diff_reload_time, NoDiffMember): reload_time = diff_reload_time.value nyan_patch_raw_api_object.add_raw_patch_member("reload_time", diff --git a/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py b/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py index f8c185d5a8..d50f33c8b8 100644 --- a/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods # @@ -379,6 +379,7 @@ def ballistics_upgrade( patches = [] + target_mode = None if value == 0: target_mode = dataset.nyan_api_objects["engine.util.target_mode.type.CurrentPosition"] @@ -1786,25 +1787,23 @@ def max_range_upgrade( game_entity_name = name_lookup_dict[head_unit_id][0] + patch_target_parent = "engine.ability.property.type.Ranged" if line.is_projectile_shooter(): - patch_target_ref = f"{game_entity_name}.Attack" + patch_target_ref = f"{game_entity_name}.Attack.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.ShootProjectile" elif line.is_melee(): if line.is_ranged(): - patch_target_ref = f"{game_entity_name}.Attack" + patch_target_ref = f"{game_entity_name}.Attack.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.RangedDiscreteEffect" else: # excludes ram upgrades return patches elif line.has_command(104): - patch_target_ref = f"{game_entity_name}.Convert" + patch_target_ref = f"{game_entity_name}.Convert.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.RangedDiscreteEffect" else: # no matching ability @@ -1899,20 +1898,18 @@ def min_range_upgrade( game_entity_name = name_lookup_dict[head_unit_id][0] + patch_target_parent = "engine.ability.property.type.Ranged" if line.is_projectile_shooter(): - patch_target_ref = f"{game_entity_name}.Attack" + patch_target_ref = f"{game_entity_name}.Attack.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.ShootProjectile" elif line.is_melee(): - patch_target_ref = f"{game_entity_name}.Attack" + patch_target_ref = f"{game_entity_name}.Attack.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.RangedDiscreteEffect" elif line.has_command(104): - patch_target_ref = f"{game_entity_name}.Convert" + patch_target_ref = f"{game_entity_name}.Convert.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) - patch_target_parent = "engine.ability.type.RangedDiscreteEffect" else: return [] diff --git a/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py index 477c52ad1e..5bcb5abb7b 100644 --- a/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods,invalid-name # @@ -839,7 +839,7 @@ def heal_range_upgrade( game_entity_name = name_lookup_dict[monk_id][0] - patch_target_ref = f"{game_entity_name}.Heal" + patch_target_ref = f"{game_entity_name}.Heal.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) # Wrapper @@ -865,7 +865,7 @@ def heal_range_upgrade( nyan_patch_raw_api_object.add_raw_patch_member("max_range", value, - "engine.ability.type.RangedContinuousEffect", + "engine.ability.property.type.Ranged", operator) patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) diff --git a/openage/convert/processor/conversion/ror/ability_subprocessor.py b/openage/convert/processor/conversion/ror/ability_subprocessor.py index 0fbb17cb17..89a634bf37 100644 --- a/openage/convert/processor/conversion/ror/ability_subprocessor.py +++ b/openage/convert/processor/conversion/ror/ability_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-branches,too-many-statements,too-many-locals # @@ -65,12 +65,7 @@ def apply_discrete_effect_ability( game_entity_name = name_lookup_dict[head_unit_id][0] ability_name = command_lookup_dict[command_id][0] - - if ranged: - ability_parent = "engine.ability.type.RangedDiscreteEffect" - - else: - ability_parent = "engine.ability.type.ApplyDiscreteEffect" + ability_parent = "engine.ability.type.ApplyDiscreteEffect" if projectile == -1: ability_ref = f"{game_entity_name}.{ability_name}" @@ -244,18 +239,35 @@ def apply_discrete_effect_ability( properties, "engine.ability.Ability") + # Range if ranged: + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + # Min range min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") # Max range max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) # Effects batch_ref = f"{ability_ref}.Batch" @@ -690,6 +702,31 @@ def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> For # Ability properties properties = {} + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + + min_range = current_unit["weapon_range_min"].value + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") + max_range = current_unit["weapon_range_max"].value + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) + # Animation ability_animation_id = current_unit["attack_sprite_id"].value if ability_animation_id > -1: @@ -796,17 +833,6 @@ def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> For max_projectiles, "engine.ability.type.ShootProjectile") - # Range - min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.ShootProjectile") - - max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.ShootProjectile") - # Reload time and delay reload_time = current_unit["attack_speed"].value ability_raw_api_object.add_raw_member("reload_time", diff --git a/openage/convert/processor/conversion/ror/upgrade_ability_subprocessor.py b/openage/convert/processor/conversion/ror/upgrade_ability_subprocessor.py index 2b00b1ad60..1d943ebd84 100644 --- a/openage/convert/processor/conversion/ror/upgrade_ability_subprocessor.py +++ b/openage/convert/processor/conversion/ror/upgrade_ability_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements # pylint: disable=too-few-public-methods,too-many-branches @@ -37,7 +37,7 @@ def shoot_projectile_ability( line: GenieGameEntityGroup, container_obj_ref: str, command_id: int, - diff: ConverterObject = None + diff: ConverterObject ) -> list[ForwardRef]: """ Creates a patch for the Selectable ability of a line. @@ -67,22 +67,79 @@ def shoot_projectile_ability( ability_name = command_lookup_dict[command_id][0] data_changed = False - if diff: - diff_animation = diff["attack_sprite_id"] - diff_comm_sound = diff["command_sound_id"] - diff_min_range = diff["weapon_range_min"] - diff_max_range = diff["weapon_range_min"] - diff_reload_time = diff["attack_speed"] - # spawn delay also depends on animation - diff_spawn_delay = diff["frame_delay"] - diff_spawn_area_offsets = diff["weapon_offset"] - - if any(not isinstance(value, NoDiffMember) for value in (diff_min_range, - diff_max_range, - diff_reload_time, - diff_spawn_delay, - diff_spawn_area_offsets)): - data_changed = True + + diff_animation = diff["attack_sprite_id"] + diff_comm_sound = diff["command_sound_id"] + diff_min_range = diff["weapon_range_min"] + diff_max_range = diff["weapon_range_min"] + diff_reload_time = diff["attack_speed"] + # spawn delay also depends on animation + diff_spawn_delay = diff["frame_delay"] + diff_spawn_area_offsets = diff["weapon_offset"] + + if any(not isinstance(value, NoDiffMember) for value in (diff_reload_time, + diff_spawn_delay, + diff_spawn_area_offsets)): + data_changed = True + + if any(not isinstance(value, NoDiffMember) for value in ( + diff_min_range, + diff_max_range + )): + patch_target_ref = f"{game_entity_name}.{ability_name}.Ranged" + patch_target_forward_ref = ForwardRef(line, patch_target_ref) + + # Wrapper + wrapper_name = f"Change{game_entity_name}{ability_name}RangedWrapper" + wrapper_ref = f"{container_obj_ref}.{wrapper_name}" + wrapper_raw_api_object = RawAPIObject(wrapper_ref, + wrapper_name, + dataset.nyan_api_objects) + wrapper_raw_api_object.add_raw_parent("engine.util.patch.Patch") + + if isinstance(line, GenieBuildingLineGroup): + wrapper_raw_api_object.set_location(("data/game_entity/generic/" + f"{name_lookup_dict[head_unit_id][1]}/")) + wrapper_raw_api_object.set_filename(f"{tech_lookup_dict[tech_id][1]}_upgrade") + + else: + wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref)) + + # Nyan patch + nyan_patch_name = f"Change{game_entity_name}{ability_name}Ranged" + nyan_patch_ref = ForwardRef(line, nyan_patch_name) + nyan_patch_location = ForwardRef(converter_group, wrapper_ref) + nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref, + nyan_patch_name, + dataset.nyan_api_objects, + nyan_patch_location) + nyan_patch_raw_api_object.add_raw_parent("engine.util.patch.NyanPatch") + nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref) + + if not isinstance(diff_min_range, NoDiffMember): + min_range = diff_min_range.value + nyan_patch_raw_api_object.add_raw_patch_member("min_range", + min_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + if not isinstance(diff_max_range, NoDiffMember): + max_range = diff_max_range.value + nyan_patch_raw_api_object.add_raw_patch_member("max_range", + max_range, + "engine.ability.property.type.Ranged", + MemberOperator.ADD) + + patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) + wrapper_raw_api_object.add_raw_member("patch", + patch_forward_ref, + "engine.util.patch.Patch") + + converter_group.add_raw_api_object(wrapper_raw_api_object) + converter_group.add_raw_api_object(nyan_patch_raw_api_object) + + wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref) + patches.append(wrapper_forward_ref) if not isinstance(diff_animation, NoDiffMember): diff_animation_id = diff_animation.value @@ -167,22 +224,6 @@ def shoot_projectile_ability( nyan_patch_raw_api_object.add_raw_parent("engine.util.patch.NyanPatch") nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref) - if not isinstance(diff_min_range, NoDiffMember): - min_range = diff_min_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("min_range", - min_range, - "engine.ability.type.ShootProjectile", - MemberOperator.ADD) - - if not isinstance(diff_max_range, NoDiffMember): - max_range = diff_max_range.value - - nyan_patch_raw_api_object.add_raw_patch_member("max_range", - max_range, - "engine.ability.type.ShootProjectile", - MemberOperator.ADD) - if not isinstance(diff_reload_time, NoDiffMember): reload_time = diff_reload_time.value diff --git a/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py b/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py index 07ca29004a..21c859027b 100644 --- a/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/ability_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2024 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-public-methods,too-many-lines,too-many-locals # pylint: disable=too-many-branches,too-many-statements,too-many-arguments @@ -108,12 +108,7 @@ def apply_discrete_effect_ability( game_entity_name = name_lookup_dict[head_unit_id][0] ability_name = command_lookup_dict[command_id][0] - - if ranged: - ability_parent = "engine.ability.type.RangedDiscreteEffect" - - else: - ability_parent = "engine.ability.type.ApplyDiscreteEffect" + ability_parent = "engine.ability.type.ApplyDiscreteEffect" if projectile == -1: ability_ref = f"{game_entity_name}.{ability_name}" @@ -268,18 +263,35 @@ def apply_discrete_effect_ability( properties, "engine.ability.Ability") + # Range if ranged: + # Range + property_ref = f"{ability_ref}.Ranged" + property_raw_api_object = RawAPIObject(property_ref, + "Ranged", + dataset.nyan_api_objects) + property_raw_api_object.add_raw_parent("engine.ability.property.type.Ranged") + property_location = ForwardRef(line, ability_ref) + property_raw_api_object.set_location(property_location) + + line.add_raw_api_object(property_raw_api_object) + # Min range min_range = current_unit["weapon_range_min"].value - ability_raw_api_object.add_raw_member("min_range", - min_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("min_range", + min_range, + "engine.ability.property.type.Ranged") # Max range max_range = current_unit["weapon_range_max"].value - ability_raw_api_object.add_raw_member("max_range", - max_range, - "engine.ability.type.RangedDiscreteEffect") + property_raw_api_object.add_raw_member("max_range", + max_range, + "engine.ability.property.type.Ranged") + + property_forward_ref = ForwardRef(line, property_ref) + properties.update({ + dataset.nyan_api_objects["engine.ability.property.type.Ranged"]: property_forward_ref + }) # Effects batch_ref = f"{ability_ref}.Batch" @@ -291,6 +303,7 @@ def apply_discrete_effect_ability( line.add_raw_api_object(batch_raw_api_object) # Effects + effects = [] if command_id == 7: # Attack if projectile != 1: @@ -1505,6 +1518,7 @@ def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: # The unit uses no gathering command or we don't recognize it continue + container_name = None if line.is_gatherer(): gatherer_unit_id = gatherer.get_id() if gatherer_unit_id not in gather_lookup_dict: diff --git a/openage/convert/processor/conversion/swgbcc/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/swgbcc/upgrade_resource_subprocessor.py index 8da0ec95ef..55c141a70a 100644 --- a/openage/convert/processor/conversion/swgbcc/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/upgrade_resource_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods # @@ -480,7 +480,7 @@ def heal_range_upgrade( game_entity_name = name_lookup_dict[medic_id][0] - patch_target_ref = f"{game_entity_name}.Heal" + patch_target_ref = f"{game_entity_name}.Heal.Ranged" patch_target_forward_ref = ForwardRef(line, patch_target_ref) # Wrapper @@ -506,7 +506,7 @@ def heal_range_upgrade( nyan_patch_raw_api_object.add_raw_patch_member("max_range", value, - "engine.ability.type.RangedContinuousEffect", + "engine.ability.property.type.Ranged", operator) patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref) 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..de5cf9a61b 100644 --- a/openage/convert/service/read/nyan_api_loader.py +++ b/openage/convert/service/read/nyan_api_loader.py @@ -1,4 +1,4 @@ -# Copyright 2019-2024 the openage authors. See copying.md for legal info. +# Copyright 2019-2025 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long,too-many-lines,too-many-statements """ @@ -104,6 +104,13 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) + # engine.ability.property.type.Ranged + parents = [api_objects["engine.ability.property.AbilityProperty"]] + nyan_object = NyanObject("Ranged", parents) + fqon = "engine.ability.property.type.Ranged" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.ability.type.ActiveTransformTo parents = [api_objects["engine.ability.Ability"]] nyan_object = NyanObject("ActiveTransformTo", parents) @@ -370,20 +377,6 @@ def _create_objects(api_objects: dict[str, NyanObject]) -> None: nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.ability.type.RangedContinuousEffect - parents = [api_objects["engine.ability.type.ApplyContinuousEffect"]] - nyan_object = NyanObject("RangedContinuousEffect", parents) - fqon = "engine.ability.type.RangedContinuousEffect" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - - # engine.ability.type.RangedDiscreteEffect - parents = [api_objects["engine.ability.type.ApplyDiscreteEffect"]] - nyan_object = NyanObject("RangedDiscreteEffect", parents) - fqon = "engine.ability.type.RangedDiscreteEffect" - nyan_object.set_fqon(fqon) - api_objects.update({fqon: nyan_object}) - # engine.ability.type.RegenerateAttribute parents = [api_objects["engine.ability.Ability"]] nyan_object = NyanObject("RegenerateAttribute", parents) @@ -539,6 +532,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.AbilityUsable + parents = [api_objects["engine.util.activity.condition.Condition"]] + nyan_object = NyanObject("AbilityUsable", parents) + fqon = "engine.util.activity.condition.type.AbilityUsable" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.activity.condition.type.CommandInQueue parents = [api_objects["engine.util.activity.condition.Condition"]] nyan_object = NyanObject("CommandInQueue", parents) @@ -546,17 +546,17 @@ 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.NextCommandIdle + # engine.util.activity.condition.type.NextCommand parents = [api_objects["engine.util.activity.condition.Condition"]] - nyan_object = NyanObject("NextCommandIdle", parents) - fqon = "engine.util.activity.condition.type.NextCommandIdle" + nyan_object = NyanObject("NextCommand", parents) + fqon = "engine.util.activity.condition.type.NextCommand" nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) - # engine.util.activity.condition.type.NextCommandMove + # engine.util.activity.condition.type.TargetInRange parents = [api_objects["engine.util.activity.condition.Condition"]] - nyan_object = NyanObject("NextCommandMove", parents) - fqon = "engine.util.activity.condition.type.NextCommandMove" + nyan_object = NyanObject("TargetInRange", parents) + fqon = "engine.util.activity.condition.type.TargetInRange" nyan_object.set_fqon(fqon) api_objects.update({fqon: nyan_object}) @@ -616,6 +616,13 @@ 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.Task + parents = [api_objects["engine.util.activity.node.Node"]] + nyan_object = NyanObject("Task", parents) + fqon = "engine.util.activity.node.type.Task" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + # engine.util.activity.node.type.XOREventGate parents = [api_objects["engine.util.activity.node.Node"]] nyan_object = NyanObject("XOREventGate", parents) @@ -630,6 +637,55 @@ 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.activity.task.Task + parents = [api_objects["engine.root.Object"]] + nyan_object = NyanObject("Task", parents) + fqon = "engine.util.activity.task.Task" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.task.type.ClearCommandQueue + parents = [api_objects["engine.util.activity.task.Task"]] + nyan_object = NyanObject("ClearCommandQueue", parents) + fqon = "engine.util.activity.task.type.ClearCommandQueue" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.task.type.MoveToTarget + parents = [api_objects["engine.util.activity.task.Task"]] + nyan_object = NyanObject("MoveToTarget", parents) + fqon = "engine.util.activity.task.type.MoveToTarget" + nyan_object.set_fqon(fqon) + api_objects.update({fqon: nyan_object}) + + # engine.util.activity.task.type.PopCommandQueue + parents = [api_objects["engine.util.activity.task.Task"]] + nyan_object = NyanObject("PopCommandQueue", parents) + fqon = "engine.util.activity.task.type.PopCommandQueue" + 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 +784,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) @@ -2574,6 +2658,14 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("lock_pool", member_type, None, None, 0) api_object.add_member(member) + # engine.ability.property.type.Ranged + api_object = api_objects["engine.ability.property.type.Ranged"] + + member = NyanMember("min_range", N_FLOAT, None, None, 0) + api_object.add_member(member) + member = NyanMember("max_range", N_FLOAT, None, None, 0) + api_object.add_member(member) + # engine.ability.type.ActiveTransformTo api_object = api_objects["engine.ability.type.ActiveTransformTo"] @@ -2713,8 +2805,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: # engine.ability.type.DetectCloak api_object = api_objects["engine.ability.type.DetectCloak"] - member = NyanMember("range", N_FLOAT, None, None, 0) - api_object.add_member(member) subtype = NyanMemberType(api_objects["engine.util.game_entity_type.GameEntityType"]) elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) member_type = NyanMemberType(MemberType.SET, (elem_type,)) @@ -2863,8 +2953,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: # engine.ability.type.Herd api_object = api_objects["engine.ability.type.Herd"] - member = NyanMember("range", N_FLOAT, None, None, 0) - api_object.add_member(member) member = NyanMember("strength", N_INT, None, None, 0) api_object.add_member(member) subtype = NyanMemberType(api_objects["engine.util.game_entity_type.GameEntityType"]) @@ -3015,22 +3103,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("amount", member_type, None, None, 0) api_object.add_member(member) - # engine.ability.type.RangedContinuousEffect - api_object = api_objects["engine.ability.type.RangedContinuousEffect"] - - member = NyanMember("min_range", N_INT, None, None, 0) - api_object.add_member(member) - member = NyanMember("max_range", N_INT, None, None, 0) - api_object.add_member(member) - - # engine.ability.type.RangedDiscreteEffect - api_object = api_objects["engine.ability.type.RangedDiscreteEffect"] - - member = NyanMember("min_range", N_INT, None, None, 0) - api_object.add_member(member) - member = NyanMember("max_range", N_INT, None, None, 0) - api_object.add_member(member) - # engine.ability.type.RegenerateAttribute api_object = api_objects["engine.ability.type.RegenerateAttribute"] @@ -3133,10 +3205,6 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: api_object.add_member(member) member = NyanMember("max_projectiles", N_INT, None, None, 0) api_object.add_member(member) - member = NyanMember("min_range", N_INT, None, None, 0) - api_object.add_member(member) - member = NyanMember("max_range", N_INT, None, None, 0) - api_object.add_member(member) member = NyanMember("reload_time", N_FLOAT, None, None, 0) api_object.add_member(member) member = NyanMember("spawn_delay", N_FLOAT, None, None, 0) @@ -3280,6 +3348,30 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("next", member_type, None, None, 0) api_object.add_member(member) + # engine.util.activity.condition.type.AbilityUsable + api_object = api_objects["engine.util.activity.condition.type.AbilityUsable"] + + subtype = NyanMemberType(api_objects["engine.ability.Ability"]) + member_type = NyanMemberType(MemberType.ABSTRACT, (subtype,)) + member = NyanMember("ability", member_type, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.condition.type.NextCommand + api_object = api_objects["engine.util.activity.condition.type.NextCommand"] + + subtype = NyanMemberType(api_objects["engine.util.command.Command"]) + member_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + member = NyanMember("command", member_type, None, None, 0) + api_object.add_member(member) + + # engine.util.activity.condition.type.TargetInRange + api_object = api_objects["engine.util.activity.condition.type.TargetInRange"] + + subtype = NyanMemberType(api_objects["engine.ability.Ability"]) + member_type = NyanMemberType(MemberType.ABSTRACT, (subtype,)) + member = NyanMember("ability", member_type, None, None, 0) + api_object.add_member(member) + # engine.util.activity.event.type.Wait api_object = api_objects["engine.util.activity.event.type.Wait"] @@ -3304,6 +3396,17 @@ def _insert_members(api_objects: dict[str, NyanObject]) -> None: member = NyanMember("next", member_type, None, None, 0) api_object.add_member(member) + # engine.util.activity.node.type.Task + api_object = api_objects["engine.util.activity.node.type.Task"] + + member_type = NyanMemberType(api_objects["engine.util.activity.node.Node"]) + member = NyanMember("next", member_type, None, None, 0) + api_object.add_member(member) + subtype = NyanMemberType(api_objects["engine.util.activity.task.Task"]) + member_type = NyanMemberType(MemberType.CHILDREN, (subtype,)) + member = NyanMember("task", member_type, None, None, 0) + api_object.add_member(member) + # engine.util.activity.node.type.XOREventGate api_object = api_objects["engine.util.activity.node.type.XOREventGate"] @@ -3324,6 +3427,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/convert/tool/api_export.py b/openage/convert/tool/api_export.py index b20752e330..e3a82ee888 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 the openage authors. See copying.md for legal info. +# Copyright 2023-2025 the openage authors. See copying.md for legal info. """ Export tool for dumping the nyan API of the engine from the converter. @@ -75,7 +75,7 @@ def create_modpack() -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("engine", modpack_version="0.5.0", versionstr="0.5.0", repo="openage") + mod_def.set_info("engine", modpack_version="0.6.0", versionstr="0.6.0", repo="openage") mod_def.add_include("**") diff --git a/openage/game/main.py b/openage/game/main.py index 1ad1378426..4f40e4fdae 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2024 the openage authors. See copying.md for legal info. +# Copyright 2015-2025 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals @@ -93,6 +93,7 @@ def main(args, error): # ensure that the openage API is present if api_export_required(asset_path): # export to assets folder + info("Updating outdated nyan API modpack") converted_path = asset_path / "converted" converted_path.mkdirs() export_api(converted_path) diff --git a/openage/main/main.py b/openage/main/main.py index 05266ec744..50f1a7ba75 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2024 the openage authors. See copying.md for legal info. +# Copyright 2015-2025 the openage authors. See copying.md for legal info. """ Main engine entry point for openage. @@ -88,6 +88,7 @@ def main(args, error): # ensure that the openage API is present if api_export_required(asset_path): # export to assets folder + info("Updating outdated nyan API modpack") converted_path = asset_path / "converted" converted_path.mkdirs() export_api(converted_path) 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(): diff --git a/openage/util/version.py b/openage/util/version.py index 712b04ec79..a9a7ffd630 100644 --- a/openage/util/version.py +++ b/openage/util/version.py @@ -1,4 +1,4 @@ -# Copyright 2024-2024 the openage authors. See copying.md for legal info. +# Copyright 2024-2025 the openage authors. See copying.md for legal info. """ Handling of version information for openage. @@ -36,26 +36,30 @@ def __init__(self, version: str) -> None: self.buildmetadata = match.group("buildmetadata") def __lt__(self, other: SemanticVersion) -> bool: - if self.major < other.major: - return True - if self.minor < other.minor: - return True - if self.patch < other.patch: - return True + if self.major != other.major: + return self.major < other.major + if self.minor != other.minor: + return self.minor < other.minor + + if self.patch != other.patch: + return self.patch < other.patch + + # else: versions are equal return False def __le__(self, other: SemanticVersion) -> bool: - if self.major <= other.major: - return True + if self.major != other.major: + return self.major < other.major - if self.minor <= other.minor: - return True + if self.minor != other.minor: + return self.minor < other.minor - if self.patch <= other.patch: - return True + if self.patch != other.patch: + return self.patch < other.patch - return False + # else: versions are equal + return True def __eq__(self, other: SemanticVersion) -> bool: return (self.major == other.major and